import * as express from 'express'
-
-import {
- processCreateActivity,
- processUpdateActivity,
- processFlagActivity
-} from '../../lib'
-import {
- Activity,
- ActivityType,
- RootActivity,
- ActivityPubCollection,
- ActivityPubOrderedCollection
-} from '../../../shared'
-import {
- signatureValidator,
- checkSignature,
- asyncMiddleware
-} from '../../middlewares'
+import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, ActivityType, RootActivity } from '../../../shared'
import { logger } from '../../helpers'
+import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
+import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib'
+import { processAddActivity } from '../../lib/activitypub/process-add'
+import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares'
+import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
Create: processCreateActivity,
+ Add: processAddActivity,
Update: processUpdateActivity,
Flag: processFlagActivity
}
inboxRouter.post('/',
signatureValidator,
asyncMiddleware(checkSignature),
- // inboxValidator,
+ activityPubValidator,
asyncMiddleware(inboxController)
)
activities = [ rootActivity as Activity ]
}
+ // Only keep activities we are able to process
+ activities = activities.filter(a => isActivityValid(a))
+
await processActivities(activities)
res.status(204).end()
-import * as express from 'express'
-
-import { database as db } from '../../../initializers/database'
-import {
- checkSignature,
- signatureValidator,
- setBodyHostPort,
- remotePodsAddValidator,
- asyncMiddleware
-} from '../../../middlewares'
-import { sendOwnedDataToPod } from '../../../lib'
-import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
-import { CONFIG } from '../../../initializers'
-import { PodInstance } from '../../../models'
-import { PodSignature, Pod as FormattedPod } from '../../../../shared'
-
-const remotePodsRouter = express.Router()
-
-remotePodsRouter.post('/remove',
- signatureValidator,
- checkSignature,
- asyncMiddleware(removePods)
-)
-
-remotePodsRouter.post('/list',
- asyncMiddleware(remotePodsList)
-)
-
-remotePodsRouter.post('/add',
- setBodyHostPort, // We need to modify the host before running the validator!
- remotePodsAddValidator,
- asyncMiddleware(addPods)
-)
-
-// ---------------------------------------------------------------------------
-
-export {
- remotePodsRouter
-}
-
-// ---------------------------------------------------------------------------
-
-async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
- const information = req.body
-
- const pod = db.Pod.build(information)
- const podCreated = await pod.save()
-
- await sendOwnedDataToPod(podCreated.id)
-
- const cert = await getMyPublicCert()
- return res.json({ cert, email: CONFIG.ADMIN.EMAIL })
-}
-
-async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) {
- const pods = await db.Pod.list()
-
- return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length))
-}
-
-async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) {
- const signature: PodSignature = req.body.signature
- const host = signature.host
-
- const pod = await db.Pod.loadByHost(host)
- await pod.destroy()
-
- return res.type('json').status(204).end()
-}
+// import * as express from 'express'
+//
+// import { database as db } from '../../../initializers/database'
+// import {
+// checkSignature,
+// signatureValidator,
+// setBodyHostPort,
+// remotePodsAddValidator,
+// asyncMiddleware
+// } from '../../../middlewares'
+// import { sendOwnedDataToPod } from '../../../lib'
+// import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
+// import { CONFIG } from '../../../initializers'
+// import { PodInstance } from '../../../models'
+// import { PodSignature, Pod as FormattedPod } from '../../../../shared'
+//
+// const remotePodsRouter = express.Router()
+//
+// remotePodsRouter.post('/remove',
+// signatureValidator,
+// checkSignature,
+// asyncMiddleware(removePods)
+// )
+//
+// remotePodsRouter.post('/list',
+// asyncMiddleware(remotePodsList)
+// )
+//
+// remotePodsRouter.post('/add',
+// setBodyHostPort, // We need to modify the host before running the validator!
+// remotePodsAddValidator,
+// asyncMiddleware(addPods)
+// )
+//
+// // ---------------------------------------------------------------------------
+//
+// export {
+// remotePodsRouter
+// }
+//
+// // ---------------------------------------------------------------------------
+//
+// async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const information = req.body
+//
+// const pod = db.Pod.build(information)
+// const podCreated = await pod.save()
+//
+// await sendOwnedDataToPod(podCreated.id)
+//
+// const cert = await getMyPublicCert()
+// return res.json({ cert, email: CONFIG.ADMIN.EMAIL })
+// }
+//
+// async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const pods = await db.Pod.list()
+//
+// return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length))
+// }
+//
+// async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const signature: PodSignature = req.body.signature
+// const host = signature.host
+//
+// const pod = await db.Pod.loadByHost(host)
+// await pod.destroy()
+//
+// return res.type('json').status(204).end()
+// }
-import * as express from 'express'
-import * as Bluebird from 'bluebird'
-import * as Sequelize from 'sequelize'
-
-import { database as db } from '../../../initializers/database'
-import {
- REQUEST_ENDPOINT_ACTIONS,
- REQUEST_ENDPOINTS,
- REQUEST_VIDEO_EVENT_TYPES,
- REQUEST_VIDEO_QADU_TYPES
-} from '../../../initializers'
-import {
- checkSignature,
- signatureValidator,
- remoteVideosValidator,
- remoteQaduVideosValidator,
- remoteEventsVideosValidator
-} from '../../../middlewares'
-import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers'
-import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib'
-import { PodInstance, VideoFileInstance } from '../../../models'
-import {
- RemoteVideoRequest,
- RemoteVideoCreateData,
- RemoteVideoUpdateData,
- RemoteVideoRemoveData,
- RemoteVideoReportAbuseData,
- RemoteQaduVideoRequest,
- RemoteQaduVideoData,
- RemoteVideoEventRequest,
- RemoteVideoEventData,
- RemoteVideoChannelCreateData,
- RemoteVideoChannelUpdateData,
- RemoteVideoChannelRemoveData,
- RemoteVideoAuthorRemoveData,
- RemoteVideoAuthorCreateData
-} from '../../../../shared'
-import { VideoInstance } from '../../../models/video/video-interface'
-
-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<any> } = {}
-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()
-
-remoteVideosRouter.post('/',
- signatureValidator,
- checkSignature,
- remoteVideosValidator,
- remoteVideos
-)
-
-remoteVideosRouter.post('/qadu',
- signatureValidator,
- checkSignature,
- remoteQaduVideosValidator,
- remoteVideosQadu
-)
-
-remoteVideosRouter.post('/events',
- signatureValidator,
- checkSignature,
- remoteEventsVideosValidator,
- remoteVideosEvents
-)
-
-// ---------------------------------------------------------------------------
-
-export {
- remoteVideosRouter
-}
-
-// ---------------------------------------------------------------------------
-
-function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
- const requests: RemoteVideoRequest[] = req.body.data
- const fromPod = res.locals.secure.pod
-
- // We need to process in the same order to keep consistency
- Bluebird.each(requests, request => {
- const data = request.data
-
- // Get the function we need to call in order to process the request
- const fun = functionsHash[request.type]
- if (fun === undefined) {
- logger.error('Unknown remote request type %s.', request.type)
- return
- }
-
- return fun.call(this, data, fromPod)
- })
- .catch(err => logger.error('Error managing remote videos.', err))
-
- // Don't block the other pod
- return res.type('json').status(204).end()
-}
-
-function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
- const requests: RemoteQaduVideoRequest[] = req.body.data
- const fromPod = res.locals.secure.pod
-
- Bluebird.each(requests, request => {
- const videoData = request.data
-
- return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
- })
- .catch(err => logger.error('Error managing remote videos.', err))
-
- return res.type('json').status(204).end()
-}
-
-function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
- const requests: RemoteVideoEventRequest[] = req.body.data
- const fromPod = res.locals.secure.pod
-
- Bluebird.each(requests, request => {
- const eventData = request.data
-
- return processVideosEventsRetryWrapper(eventData, fromPod)
- })
- .catch(err => logger.error('Error managing remote videos.', err))
-
- return res.type('json').status(204).end()
-}
-
-async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
- const options = {
- arguments: [ eventData, fromPod ],
- errorMessage: 'Cannot process videos events with many retries.'
- }
-
- await retryTransactionWrapper(processVideosEvents, options)
-}
-
-async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
- await db.sequelize.transaction(async t => {
- const sequelizeOptions = { transaction: t }
- const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t)
-
- let columnToUpdate
- let qaduType
-
- switch (eventData.eventType) {
- case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
- columnToUpdate = 'views'
- qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
- break
-
- case REQUEST_VIDEO_EVENT_TYPES.LIKES:
- columnToUpdate = 'likes'
- qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
- break
-
- case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
- columnToUpdate = 'dislikes'
- qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
- break
-
- default:
- throw new Error('Unknown video event type.')
- }
-
- const query = {}
- query[columnToUpdate] = eventData.count
-
- await videoInstance.increment(query, sequelizeOptions)
-
- const qadusParams = [
- {
- videoId: videoInstance.id,
- type: qaduType
- }
- ]
- await quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
- })
-
- logger.info('Remote video event processed for video with uuid %s.', eventData.uuid)
-}
-
-async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoData, fromPod ],
- errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
- }
-
- await retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
-}
-
-async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
- let videoUUID = ''
-
- await db.sequelize.transaction(async t => {
- const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
- const sequelizeOptions = { transaction: t }
-
- videoUUID = videoInstance.uuid
-
- if (videoData.views) {
- videoInstance.set('views', videoData.views)
- }
-
- if (videoData.likes) {
- videoInstance.set('likes', videoData.likes)
- }
-
- if (videoData.dislikes) {
- videoInstance.set('dislikes', videoData.dislikes)
- }
-
- await videoInstance.save(sequelizeOptions)
- })
-
- logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
-}
-
-// Handle retries on fail
-async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoToCreateData, fromPod ],
- errorMessage: 'Cannot insert the remote video with many retries.'
- }
-
- await retryTransactionWrapper(addRemoteVideo, options)
-}
-
-async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) {
- logger.debug('Adding remote video "%s".', videoToCreateData.uuid)
-
- await db.sequelize.transaction(async t => {
- const sequelizeOptions = {
- transaction: t
- }
-
- const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid)
- if (videoFromDatabase) throw new Error('UUID already exists.')
-
- const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
- if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
-
- const tags = videoToCreateData.tags
- const tagInstances = await db.Tag.findOrCreateTags(tags, t)
-
- const videoData = {
- name: videoToCreateData.name,
- uuid: videoToCreateData.uuid,
- category: videoToCreateData.category,
- licence: videoToCreateData.licence,
- language: videoToCreateData.language,
- nsfw: videoToCreateData.nsfw,
- description: videoToCreateData.truncatedDescription,
- channelId: videoChannel.id,
- duration: videoToCreateData.duration,
- createdAt: videoToCreateData.createdAt,
- // FIXME: updatedAt does not seems to be considered by Sequelize
- updatedAt: videoToCreateData.updatedAt,
- views: videoToCreateData.views,
- likes: videoToCreateData.likes,
- dislikes: videoToCreateData.dislikes,
- remote: true,
- privacy: videoToCreateData.privacy
- }
-
- const video = db.Video.build(videoData)
- await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
- const videoCreated = await video.save(sequelizeOptions)
-
- const tasks = []
- for (const fileData of videoToCreateData.files) {
- const videoFileInstance = db.VideoFile.build({
- extname: fileData.extname,
- infoHash: fileData.infoHash,
- resolution: fileData.resolution,
- size: fileData.size,
- videoId: videoCreated.id
- })
-
- tasks.push(videoFileInstance.save(sequelizeOptions))
- }
-
- await Promise.all(tasks)
-
- await videoCreated.setTags(tagInstances, sequelizeOptions)
- })
-
- logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
-}
-
-// Handle retries on fail
-async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoAttributesToUpdate, fromPod ],
- errorMessage: 'Cannot update the remote video with many retries'
- }
-
- await retryTransactionWrapper(updateRemoteVideo, options)
-}
-
-async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) {
- logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
- let videoInstance: VideoInstance
- let videoFieldsSave: object
-
- try {
- await db.sequelize.transaction(async t => {
- const sequelizeOptions = {
- transaction: t
- }
-
- const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t)
- videoFieldsSave = videoInstance.toJSON()
- const tags = videoAttributesToUpdate.tags
-
- const tagInstances = await db.Tag.findOrCreateTags(tags, t)
-
- videoInstance.set('name', videoAttributesToUpdate.name)
- videoInstance.set('category', videoAttributesToUpdate.category)
- videoInstance.set('licence', videoAttributesToUpdate.licence)
- videoInstance.set('language', videoAttributesToUpdate.language)
- videoInstance.set('nsfw', videoAttributesToUpdate.nsfw)
- videoInstance.set('description', videoAttributesToUpdate.truncatedDescription)
- videoInstance.set('duration', videoAttributesToUpdate.duration)
- videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
- videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
- videoInstance.set('views', videoAttributesToUpdate.views)
- videoInstance.set('likes', videoAttributesToUpdate.likes)
- videoInstance.set('dislikes', videoAttributesToUpdate.dislikes)
- videoInstance.set('privacy', videoAttributesToUpdate.privacy)
-
- await videoInstance.save(sequelizeOptions)
-
- // Remove old video files
- const videoFileDestroyTasks: Bluebird<void>[] = []
- for (const videoFile of videoInstance.VideoFiles) {
- videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
- }
- await Promise.all(videoFileDestroyTasks)
-
- const videoFileCreateTasks: Bluebird<VideoFileInstance>[] = []
- for (const fileData of videoAttributesToUpdate.files) {
- const videoFileInstance = db.VideoFile.build({
- extname: fileData.extname,
- infoHash: fileData.infoHash,
- resolution: fileData.resolution,
- size: fileData.size,
- videoId: videoInstance.id
- })
-
- videoFileCreateTasks.push(videoFileInstance.save(sequelizeOptions))
- }
-
- await Promise.all(videoFileCreateTasks)
-
- await videoInstance.setTags(tagInstances, sequelizeOptions)
- })
-
- logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
- } catch (err) {
- if (videoInstance !== undefined && videoFieldsSave !== undefined) {
- resetSequelizeInstance(videoInstance, videoFieldsSave)
- }
-
- // This is just a debug because we will retry the insert
- logger.debug('Cannot update the remote video.', err)
- throw err
- }
-}
-
-async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoToRemoveData, fromPod ],
- errorMessage: 'Cannot remove the remote video channel with many retries.'
- }
-
- await retryTransactionWrapper(removeRemoteVideo, options)
-}
-
-async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
- logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
-
- await db.sequelize.transaction(async t => {
- // We need the instance because we have to remove some other stuffs (thumbnail etc)
- const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
- await videoInstance.destroy({ transaction: t })
- })
-
- logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
-}
-
-async function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
- const options = {
- arguments: [ authorToCreateData, fromPod ],
- errorMessage: 'Cannot insert the remote video author with many retries.'
- }
-
- await retryTransactionWrapper(addRemoteVideoAuthor, options)
-}
-
-async function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
- logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
-
- await db.sequelize.transaction(async t => {
- const authorInDatabase = await db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
- if (authorInDatabase) throw new Error('Author with UUID ' + authorToCreateData.uuid + ' already exists.')
-
- const videoAuthorData = {
- name: authorToCreateData.name,
- uuid: authorToCreateData.uuid,
- userId: null, // Not on our pod
- podId: fromPod.id
- }
-
- const author = db.Author.build(videoAuthorData)
- await author.save({ transaction: t })
- })
-
- logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid)
-}
-
-async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
- const options = {
- arguments: [ authorAttributesToRemove, fromPod ],
- errorMessage: 'Cannot remove the remote video author with many retries.'
- }
-
- await retryTransactionWrapper(removeRemoteVideoAuthor, options)
-}
-
-async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
- logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
-
- await db.sequelize.transaction(async t => {
- const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
- await videoAuthor.destroy({ transaction: t })
- })
-
- logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)
-}
-
-async function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoChannelToCreateData, fromPod ],
- errorMessage: 'Cannot insert the remote video channel with many retries.'
- }
-
- await retryTransactionWrapper(addRemoteVideoChannel, options)
-}
-
-async function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
- logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
-
- await db.sequelize.transaction(async t => {
- const videoChannelInDatabase = await db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
- if (videoChannelInDatabase) {
- throw new Error('Video channel with UUID ' + videoChannelToCreateData.uuid + ' already exists.')
- }
-
- const authorUUID = videoChannelToCreateData.ownerUUID
- const podId = fromPod.id
-
- const author = await db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
- if (!author) throw new Error('Unknown author UUID' + authorUUID + '.')
-
- 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)
- await videoChannel.save({ transaction: t })
- })
-
- logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
-}
-
-async function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoChannelAttributesToUpdate, fromPod ],
- errorMessage: 'Cannot update the remote video channel with many retries.'
- }
-
- await retryTransactionWrapper(updateRemoteVideoChannel, options)
-}
-
-async function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
- logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
-
- await db.sequelize.transaction(async t => {
- const sequelizeOptions = { transaction: t }
-
- const videoChannelInstance = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
- videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
- videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
- videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
- videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
-
- await videoChannelInstance.save(sequelizeOptions)
- })
-
- logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid)
-}
-
-async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
- const options = {
- arguments: [ videoChannelAttributesToRemove, fromPod ],
- errorMessage: 'Cannot remove the remote video channel with many retries.'
- }
-
- await retryTransactionWrapper(removeRemoteVideoChannel, options)
-}
-
-async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
- logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
-
- await db.sequelize.transaction(async t => {
- const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
- await videoChannel.destroy({ transaction: t })
- })
-
- logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
-}
-
-async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
- const options = {
- arguments: [ reportData, fromPod ],
- errorMessage: 'Cannot create remote abuse video with many retries.'
- }
-
- await retryTransactionWrapper(reportAbuseRemoteVideo, options)
-}
-
-async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
- logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
-
- await db.sequelize.transaction(async t => {
- const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t)
- const videoAbuseData = {
- reporterUsername: reportData.reporterUsername,
- reason: reportData.reportReason,
- reporterPodId: fromPod.id,
- videoId: videoInstance.id
- }
-
- await db.VideoAbuse.create(videoAbuseData)
-
- })
-
- logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)
-}
-
-async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) {
- try {
- const video = await db.Video.loadLocalVideoByUUID(id, t)
-
- if (!video) throw new Error('Video ' + id + ' not found')
-
- return video
- } catch (err) {
- logger.error('Cannot load owned video from id.', { error: err.stack, id })
- throw err
- }
-}
-
-async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
- try {
- const video = await db.Video.loadByHostAndUUID(podHost, uuid, t)
- if (!video) throw new Error('Video not found')
-
- return video
- } catch (err) {
- logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
- throw err
- }
-}
+// import * as express from 'express'
+// import * as Bluebird from 'bluebird'
+// import * as Sequelize from 'sequelize'
+//
+// import { database as db } from '../../../initializers/database'
+// import {
+// REQUEST_ENDPOINT_ACTIONS,
+// REQUEST_ENDPOINTS,
+// REQUEST_VIDEO_EVENT_TYPES,
+// REQUEST_VIDEO_QADU_TYPES
+// } from '../../../initializers'
+// import {
+// checkSignature,
+// signatureValidator,
+// remoteVideosValidator,
+// remoteQaduVideosValidator,
+// remoteEventsVideosValidator
+// } from '../../../middlewares'
+// import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers'
+// import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib'
+// import { PodInstance, VideoFileInstance } from '../../../models'
+// import {
+// RemoteVideoRequest,
+// RemoteVideoCreateData,
+// RemoteVideoUpdateData,
+// RemoteVideoRemoveData,
+// RemoteVideoReportAbuseData,
+// RemoteQaduVideoRequest,
+// RemoteQaduVideoData,
+// RemoteVideoEventRequest,
+// RemoteVideoEventData,
+// RemoteVideoChannelCreateData,
+// RemoteVideoChannelUpdateData,
+// RemoteVideoChannelRemoveData,
+// RemoteVideoAuthorRemoveData,
+// RemoteVideoAuthorCreateData
+// } from '../../../../shared'
+// import { VideoInstance } from '../../../models/video/video-interface'
+//
+// 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<any> } = {}
+// 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()
+//
+// remoteVideosRouter.post('/',
+// signatureValidator,
+// checkSignature,
+// remoteVideosValidator,
+// remoteVideos
+// )
+//
+// remoteVideosRouter.post('/qadu',
+// signatureValidator,
+// checkSignature,
+// remoteQaduVideosValidator,
+// remoteVideosQadu
+// )
+//
+// remoteVideosRouter.post('/events',
+// signatureValidator,
+// checkSignature,
+// remoteEventsVideosValidator,
+// remoteVideosEvents
+// )
+//
+// // ---------------------------------------------------------------------------
+//
+// export {
+// remoteVideosRouter
+// }
+//
+// // ---------------------------------------------------------------------------
+//
+// function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const requests: RemoteVideoRequest[] = req.body.data
+// const fromPod = res.locals.secure.pod
+//
+// // We need to process in the same order to keep consistency
+// Bluebird.each(requests, request => {
+// const data = request.data
+//
+// // Get the function we need to call in order to process the request
+// const fun = functionsHash[request.type]
+// if (fun === undefined) {
+// logger.error('Unknown remote request type %s.', request.type)
+// return
+// }
+//
+// return fun.call(this, data, fromPod)
+// })
+// .catch(err => logger.error('Error managing remote videos.', err))
+//
+// // Don't block the other pod
+// return res.type('json').status(204).end()
+// }
+//
+// function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const requests: RemoteQaduVideoRequest[] = req.body.data
+// const fromPod = res.locals.secure.pod
+//
+// Bluebird.each(requests, request => {
+// const videoData = request.data
+//
+// return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
+// })
+// .catch(err => logger.error('Error managing remote videos.', err))
+//
+// return res.type('json').status(204).end()
+// }
+//
+// function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
+// const requests: RemoteVideoEventRequest[] = req.body.data
+// const fromPod = res.locals.secure.pod
+//
+// Bluebird.each(requests, request => {
+// const eventData = request.data
+//
+// return processVideosEventsRetryWrapper(eventData, fromPod)
+// })
+// .catch(err => logger.error('Error managing remote videos.', err))
+//
+// return res.type('json').status(204).end()
+// }
+//
+// async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ eventData, fromPod ],
+// errorMessage: 'Cannot process videos events with many retries.'
+// }
+//
+// await retryTransactionWrapper(processVideosEvents, options)
+// }
+//
+// async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
+// await db.sequelize.transaction(async t => {
+// const sequelizeOptions = { transaction: t }
+// const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t)
+//
+// let columnToUpdate
+// let qaduType
+//
+// switch (eventData.eventType) {
+// case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
+// columnToUpdate = 'views'
+// qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
+// break
+//
+// case REQUEST_VIDEO_EVENT_TYPES.LIKES:
+// columnToUpdate = 'likes'
+// qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
+// break
+//
+// case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
+// columnToUpdate = 'dislikes'
+// qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
+// break
+//
+// default:
+// throw new Error('Unknown video event type.')
+// }
+//
+// const query = {}
+// query[columnToUpdate] = eventData.count
+//
+// await videoInstance.increment(query, sequelizeOptions)
+//
+// const qadusParams = [
+// {
+// videoId: videoInstance.id,
+// type: qaduType
+// }
+// ]
+// await quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
+// })
+//
+// logger.info('Remote video event processed for video with uuid %s.', eventData.uuid)
+// }
+//
+// async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ videoData, fromPod ],
+// errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
+// }
+//
+// await retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
+// }
+//
+// async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
+// let videoUUID = ''
+//
+// await db.sequelize.transaction(async t => {
+// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
+// const sequelizeOptions = { transaction: t }
+//
+// videoUUID = videoInstance.uuid
+//
+// if (videoData.views) {
+// videoInstance.set('views', videoData.views)
+// }
+//
+// if (videoData.likes) {
+// videoInstance.set('likes', videoData.likes)
+// }
+//
+// if (videoData.dislikes) {
+// videoInstance.set('dislikes', videoData.dislikes)
+// }
+//
+// await videoInstance.save(sequelizeOptions)
+// })
+//
+// logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
+// }
+//
+// async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ videoToRemoveData, fromPod ],
+// errorMessage: 'Cannot remove the remote video channel with many retries.'
+// }
+//
+// await retryTransactionWrapper(removeRemoteVideo, options)
+// }
+//
+// async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
+// logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
+//
+// await db.sequelize.transaction(async t => {
+// // We need the instance because we have to remove some other stuffs (thumbnail etc)
+// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
+// await videoInstance.destroy({ transaction: t })
+// })
+//
+// logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
+// }
+//
+// async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ authorAttributesToRemove, fromPod ],
+// errorMessage: 'Cannot remove the remote video author with many retries.'
+// }
+//
+// await retryTransactionWrapper(removeRemoteVideoAuthor, options)
+// }
+//
+// async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
+// logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
+//
+// await db.sequelize.transaction(async t => {
+// const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
+// await videoAuthor.destroy({ transaction: t })
+// })
+//
+// logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)
+// }
+//
+// async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ videoChannelAttributesToRemove, fromPod ],
+// errorMessage: 'Cannot remove the remote video channel with many retries.'
+// }
+//
+// await retryTransactionWrapper(removeRemoteVideoChannel, options)
+// }
+//
+// async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
+// logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
+//
+// await db.sequelize.transaction(async t => {
+// const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
+// await videoChannel.destroy({ transaction: t })
+// })
+//
+// logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
+// }
+//
+// async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
+// const options = {
+// arguments: [ reportData, fromPod ],
+// errorMessage: 'Cannot create remote abuse video with many retries.'
+// }
+//
+// await retryTransactionWrapper(reportAbuseRemoteVideo, options)
+// }
+//
+// async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
+// logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
+//
+// await db.sequelize.transaction(async t => {
+// const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t)
+// const videoAbuseData = {
+// reporterUsername: reportData.reporterUsername,
+// reason: reportData.reportReason,
+// reporterPodId: fromPod.id,
+// videoId: videoInstance.id
+// }
+//
+// await db.VideoAbuse.create(videoAbuseData)
+//
+// })
+//
+// logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)
+// }
+//
+// async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) {
+// try {
+// const video = await db.Video.loadLocalVideoByUUID(id, t)
+//
+// if (!video) throw new Error('Video ' + id + ' not found')
+//
+// return video
+// } catch (err) {
+// logger.error('Cannot load owned video from id.', { error: err.stack, id })
+// throw err
+// }
+// }
+//
+// async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
+// try {
+// const video = await db.Video.loadByHostAndUUID(podHost, uuid, t)
+// if (!video) throw new Error('Video not found')
+//
+// return video
+// } catch (err) {
+// logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
+// throw err
+// }
+// }
VIDEO_CATEGORIES,
VIDEO_LICENCES,
VIDEO_LANGUAGES,
- VIDEO_PRIVACIES
+ VIDEO_PRIVACIES,
+ VIDEO_MIMETYPE_EXT
} from '../../../initializers'
import {
addEventToRemoteVideo,
import { blacklistRouter } from './blacklist'
import { rateVideoRouter } from './rate'
import { videoChannelRouter } from './channel'
+import { getActivityPubUrl } from '../../../helpers/activitypub'
const videosRouter = express.Router()
cb(null, CONFIG.STORAGE.VIDEOS_DIR)
},
- filename: (req, file, cb) => {
- let extension = ''
- if (file.mimetype === 'video/webm') extension = 'webm'
- else if (file.mimetype === 'video/mp4') extension = 'mp4'
- else if (file.mimetype === 'video/ogg') extension = 'ogv'
- generateRandomString(16)
- .then(randomString => {
- cb(null, randomString + '.' + extension)
- })
- .catch(err => {
- logger.error('Cannot generate random string for file name.', err)
- throw err
- })
+ filename: async (req, file, cb) => {
+ const extension = VIDEO_MIMETYPE_EXT[file.mimetype]
+ let randomString = ''
+
+ try {
+ randomString = await generateRandomString(16)
+ } catch (err) {
+ logger.error('Cannot generate random string for file name.', err)
+ randomString = 'fake-random-string'
+ }
+
+ cb(null, randomString + '.' + extension)
}
})
channelId: res.locals.videoChannel.id
}
const video = db.Video.build(videoData)
+ video.url = getActivityPubUrl('video', video.uuid)
const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
const videoFileHeight = await getVideoFileHeight(videoFilePath)
import { database as db } from '../initializers'
import { logger } from './logger'
-import { doRequest } from './requests'
+import { doRequest, doRequestAndSaveToFile } from './requests'
import { isRemoteAccountValid } from './custom-validators'
import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
import { ResultList } from '../../shared/models/result-list.model'
+import { CONFIG } from '../initializers/constants'
+import { VideoInstance } from '../models/video/video-interface'
+import { ActivityIconObject } from '../../shared/index'
+import { join } from 'path'
+
+function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
+ const thumbnailName = video.getThumbnailName()
+ const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
+
+ const options = {
+ method: 'GET',
+ uri: icon.url
+ }
+ return doRequestAndSaveToFile(options, thumbnailPath)
+}
+
+function getActivityPubUrl (type: 'video' | 'videoChannel', uuid: string) {
+ if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + uuid
+ else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + uuid
+
+ return ''
+}
+
+async function getOrCreateAccount (accountUrl: string) {
+ let account = await db.Account.loadByUrl(accountUrl)
+
+ // We don't have this account in our database, fetch it on remote
+ if (!account) {
+ const { account } = await fetchRemoteAccountAndCreatePod(accountUrl)
+
+ if (!account) throw new Error('Cannot fetch remote account.')
+
+ // Save our new account in database
+ await account.save()
+ }
+
+ return account
+}
async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
const options = {
export {
fetchRemoteAccountAndCreatePod,
activityPubContextify,
- activityPubCollectionPagination
+ activityPubCollectionPagination,
+ getActivityPubUrl,
+ generateThumbnailFromUrl,
+ getOrCreateAccount
}
// ---------------------------------------------------------------------------
--- /dev/null
+import * as validator from 'validator'
+import {
+ isVideoChannelCreateActivityValid,
+ isVideoTorrentAddActivityValid,
+ isVideoTorrentUpdateActivityValid,
+ isVideoChannelUpdateActivityValid
+} from './videos'
+
+function isRootActivityValid (activity: any) {
+ return Array.isArray(activity['@context']) &&
+ (
+ (activity.type === 'Collection' || activity.type === 'OrderedCollection') &&
+ validator.isInt(activity.totalItems, { min: 0 }) &&
+ Array.isArray(activity.items)
+ ) ||
+ (
+ validator.isURL(activity.id) &&
+ validator.isURL(activity.actor)
+ )
+}
+
+function isActivityValid (activity: any) {
+ return isVideoTorrentAddActivityValid(activity) ||
+ isVideoChannelCreateActivityValid(activity) ||
+ isVideoTorrentUpdateActivityValid(activity) ||
+ isVideoChannelUpdateActivityValid(activity)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isRootActivityValid,
+ isActivityValid
+}
export * from './account'
+export * from './activity'
export * from './signature'
export * from './misc'
export * from './videos'
return exists(url) && validator.isURL(url, isURLOptions)
}
+function isBaseActivityValid (activity: any, type: string) {
+ return Array.isArray(activity['@context']) &&
+ activity.type === type &&
+ validator.isURL(activity.id) &&
+ validator.isURL(activity.actor) &&
+ Array.isArray(activity.to) &&
+ activity.to.every(t => validator.isURL(t))
+}
+
export {
- isActivityPubUrlValid
+ isActivityPubUrlValid,
+ isBaseActivityValid
}
-import 'express-validator'
-import { has, values } from 'lodash'
+import * as validator from 'validator'
import {
- REQUEST_ENDPOINTS,
- REQUEST_ENDPOINT_ACTIONS,
- REQUEST_VIDEO_EVENT_TYPES
+ ACTIVITY_PUB
} from '../../../initializers'
-import { isArray, isDateValid, isUUIDValid } from '../misc'
+import { isDateValid, isUUIDValid } from '../misc'
import {
- isVideoThumbnailDataValid,
- isVideoAbuseReasonValid,
- isVideoAbuseReporterUsernameValid,
isVideoViewsValid,
- isVideoLikesValid,
- isVideoDislikesValid,
- isVideoEventCountValid,
- isRemoteVideoCategoryValid,
- isRemoteVideoLicenceValid,
- isRemoteVideoLanguageValid,
isVideoNSFWValid,
isVideoTruncatedDescriptionValid,
isVideoDurationValid,
- isVideoFileInfoHashValid,
isVideoNameValid,
- isVideoTagsValid,
- isVideoFileExtnameValid,
- isVideoFileResolutionValid
+ isVideoTagValid
} 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 removeBadRequestVideos (requests: any[]) {
- for (let i = requests.length - 1; i >= 0 ; i--) {
- const request = requests[i]
- const video = request.data
-
- if (
- !video ||
- checkers[request.type] === undefined ||
- checkers[request.type](video) === false
- ) {
- requests.splice(i, 1)
- }
- }
+import { isBaseActivityValid } from './misc'
+
+function isVideoTorrentAddActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Add') &&
+ isVideoTorrentObjectValid(activity.object)
+}
+
+function isVideoTorrentUpdateActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Update') &&
+ isVideoTorrentObjectValid(activity.object)
}
-function removeBadRequestVideosQadu (requests: any[]) {
- for (let i = requests.length - 1; i >= 0 ; i--) {
- const request = requests[i]
- const video = request.data
-
- if (
- !video ||
- (
- isUUIDValid(video.uuid) &&
- (has(video, 'views') === false || isVideoViewsValid(video.views)) &&
- (has(video, 'likes') === false || isVideoLikesValid(video.likes)) &&
- (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes))
- ) === false
- ) {
- requests.splice(i, 1)
- }
- }
+function isVideoTorrentObjectValid (video: any) {
+ return video.type === 'Video' &&
+ isVideoNameValid(video.name) &&
+ isVideoDurationValid(video.duration) &&
+ isUUIDValid(video.uuid) &&
+ setValidRemoteTags(video) &&
+ isRemoteIdentifierValid(video.category) &&
+ isRemoteIdentifierValid(video.licence) &&
+ isRemoteIdentifierValid(video.language) &&
+ isVideoViewsValid(video.video) &&
+ isVideoNSFWValid(video.nsfw) &&
+ isDateValid(video.published) &&
+ isDateValid(video.updated) &&
+ isRemoteVideoContentValid(video.mediaType, video.content) &&
+ isRemoteVideoIconValid(video.icon) &&
+ setValidRemoteVideoUrls(video.url)
}
-function removeBadRequestVideosEvents (requests: any[]) {
- for (let i = requests.length - 1; i >= 0 ; i--) {
- const request = requests[i]
- const eventData = request.data
-
- if (
- !eventData ||
- (
- isUUIDValid(eventData.uuid) &&
- values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 &&
- isVideoEventCountValid(eventData.count)
- ) === false
- ) {
- requests.splice(i, 1)
- }
- }
+function isVideoChannelCreateActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Create') &&
+ isVideoChannelObjectValid(activity.object)
+}
+
+function isVideoChannelUpdateActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Update') &&
+ isVideoChannelObjectValid(activity.object)
+}
+
+function isVideoChannelObjectValid (videoChannel: any) {
+ return videoChannel.type === 'VideoChannel' &&
+ isVideoChannelNameValid(videoChannel.name) &&
+ isVideoChannelDescriptionValid(videoChannel.description) &&
+ isUUIDValid(videoChannel.uuid)
}
// ---------------------------------------------------------------------------
export {
- removeBadRequestVideos,
- removeBadRequestVideosQadu,
- removeBadRequestVideosEvents
+ isVideoTorrentAddActivityValid,
+ isVideoChannelCreateActivityValid,
+ isVideoTorrentUpdateActivityValid,
+ isVideoChannelUpdateActivityValid
}
// ---------------------------------------------------------------------------
-function isCommonVideoAttributesValid (video: any) {
- return isDateValid(video.createdAt) &&
- isDateValid(video.updatedAt) &&
- isRemoteVideoCategoryValid(video.category) &&
- isRemoteVideoLicenceValid(video.licence) &&
- isRemoteVideoLanguageValid(video.language) &&
- isVideoNSFWValid(video.nsfw) &&
- isVideoTruncatedDescriptionValid(video.truncatedDescription) &&
- isVideoDurationValid(video.duration) &&
- isVideoNameValid(video.name) &&
- isVideoTagsValid(video.tags) &&
- isUUIDValid(video.uuid) &&
- isVideoViewsValid(video.views) &&
- isVideoLikesValid(video.likes) &&
- isVideoDislikesValid(video.dislikes) &&
- isArray(video.files) &&
- video.files.every(videoFile => {
- if (!videoFile) return false
-
- return (
- isVideoFileInfoHashValid(videoFile.infoHash) &&
- isVideoFileExtnameValid(videoFile.extname) &&
- isVideoFileResolutionValid(videoFile.resolution)
- )
- })
-}
+function setValidRemoteTags (video: any) {
+ if (Array.isArray(video.tag) === false) return false
-function checkAddVideo (video: any) {
- return isCommonVideoAttributesValid(video) &&
- isUUIDValid(video.channelUUID) &&
- isVideoThumbnailDataValid(video.thumbnailData)
-}
+ const newTag = video.tag.filter(t => {
+ return t.type === 'Hashtag' &&
+ isVideoTagValid(t.name)
+ })
-function checkUpdateVideo (video: any) {
- return isCommonVideoAttributesValid(video)
+ video.tag = newTag
+ return true
}
-function checkRemoveVideo (video: any) {
- return isUUIDValid(video.uuid)
+function isRemoteIdentifierValid (data: any) {
+ return validator.isInt(data.identifier, { min: 0 })
}
-function checkReportVideo (abuse: any) {
- return isUUIDValid(abuse.videoUUID) &&
- isVideoAbuseReasonValid(abuse.reportReason) &&
- isVideoAbuseReporterUsernameValid(abuse.reporterUsername)
+function isRemoteVideoContentValid (mediaType: string, content: string) {
+ return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
}
-function checkAddVideoChannel (videoChannel: any) {
- return isUUIDValid(videoChannel.uuid) &&
- isVideoChannelNameValid(videoChannel.name) &&
- isVideoChannelDescriptionValid(videoChannel.description) &&
- isDateValid(videoChannel.createdAt) &&
- isDateValid(videoChannel.updatedAt) &&
- isUUIDValid(videoChannel.ownerUUID)
+function isRemoteVideoIconValid (icon: any) {
+ return icon.type === 'Image' &&
+ validator.isURL(icon.url) &&
+ icon.mediaType === 'image/jpeg' &&
+ validator.isInt(icon.width, { min: 0 }) &&
+ validator.isInt(icon.height, { min: 0 })
}
-function checkUpdateVideoChannel (videoChannel: any) {
- return isUUIDValid(videoChannel.uuid) &&
- isVideoChannelNameValid(videoChannel.name) &&
- isVideoChannelDescriptionValid(videoChannel.description) &&
- isDateValid(videoChannel.createdAt) &&
- isDateValid(videoChannel.updatedAt) &&
- isUUIDValid(videoChannel.ownerUUID)
-}
+function setValidRemoteVideoUrls (video: any) {
+ if (Array.isArray(video.url) === false) return false
-function checkRemoveVideoChannel (videoChannel: any) {
- return isUUIDValid(videoChannel.uuid)
-}
+ const newUrl = video.url.filter(u => isRemoteVideoUrlValid(u))
+ video.url = newUrl
-function checkAddAuthor (author: any) {
- return isUUIDValid(author.uuid) &&
- isVideoAuthorNameValid(author.name)
+ return true
}
-function checkRemoveAuthor (author: any) {
- return isUUIDValid(author.uuid)
+function isRemoteVideoUrlValid (url: any) {
+ return url.type === 'Link' &&
+ ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 &&
+ validator.isURL(url.url) &&
+ validator.isInt(url.width, { min: 0 }) &&
+ validator.isInt(url.size, { min: 0 })
}
export * from './pods'
export * from './pods'
export * from './users'
-export * from './video-authors'
export * from './video-channels'
export * from './videos'
+++ /dev/null
-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<AuthorInstance>
- 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
-}
}
function isVideoDurationValid (value: string) {
- return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION)
+ // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
+ return exists(value) &&
+ typeof value === 'string' &&
+ value.startsWith('PT') &&
+ value.endsWith('S') &&
+ validator.isInt(value.replace(/[^0-9]+/, ''), VIDEOS_CONSTRAINTS_FIELDS.DURATION)
}
function isVideoNameValid (value: string) {
return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
}
+function isVideoTagValid (tag: string) {
+ return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
+}
+
function isVideoTagsValid (tags: string[]) {
return isArray(tags) &&
validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
- tags.every(tag => {
- return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
- })
+ tags.every(tag => isVideoTagValid(tag))
}
function isVideoThumbnailValid (value: string) {
isRemoteVideoPrivacyValid,
isVideoFileResolutionValid,
checkVideoExists,
+ isVideoTagValid,
isRemoteVideoCategoryValid,
isRemoteVideoLicenceValid,
isRemoteVideoLanguageValid
import { PodInstance } from '../models'
import { PodSignature } from '../../shared'
import { signObject } from './peertube-crypto'
+import { createWriteStream } from 'fs'
function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
})
}
+function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) {
+ return new Promise<request.RequestResponse>((res, rej) => {
+ request(requestOptions)
+ .on('response', response => res(response as request.RequestResponse))
+ .on('error', err => rej(err))
+ .pipe(createWriteStream(destPath))
+ })
+}
+
type MakeRetryRequestParams = {
url: string,
method: 'GET' | 'POST',
export {
doRequest,
+ doRequestAndSaveToFile,
makeRetryRequest,
makeSecureRequest
}
[VideoPrivacy.PRIVATE]: 'Private'
}
+const VIDEO_MIMETYPE_EXT = {
+ 'video/webm': 'webm',
+ 'video/ogg': 'ogv',
+ 'video/mp4': 'mp4'
+}
+
// ---------------------------------------------------------------------------
// Score a pod has when we create it as a friend
}
const ACTIVITY_PUB = {
- COLLECTION_ITEMS_PER_PAGE: 10
+ COLLECTION_ITEMS_PER_PAGE: 10,
+ VIDEO_URL_MIME_TYPES: [
+ 'video/mp4',
+ 'video/webm',
+ 'video/ogg',
+ 'application/x-bittorrent',
+ 'application/x-bittorrent;x-scheme-handler/magnet'
+ ]
}
// ---------------------------------------------------------------------------
// Number of requests to retry for replay requests module
const RETRY_REQUESTS = 5
-const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = {
- VIDEOS: 'videos'
-}
-
-const REQUEST_ENDPOINT_ACTIONS: {
- [ id: string ]: {
- [ id: string ]: RemoteVideoRequestType
- }
-} = {}
-REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = {
- 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'
-}
-
-const REQUEST_VIDEO_QADU_ENDPOINT = 'videos/qadu'
-const REQUEST_VIDEO_EVENT_ENDPOINT = 'videos/events'
-
-const REQUEST_VIDEO_QADU_TYPES: { [ id: string ]: RequestVideoQaduType } = {
- LIKES: 'likes',
- DISLIKES: 'dislikes',
- VIEWS: 'views'
-}
-
-const REQUEST_VIDEO_EVENT_TYPES: { [ id: string ]: RequestVideoEventType } = {
- LIKES: 'likes',
- DISLIKES: 'dislikes',
- VIEWS: 'views'
-}
-
const REMOTE_SCHEME = {
HTTP: 'https',
WS: 'wss'
// ---------------------------------------------------------------------------
-// const SIGNATURE_ALGORITHM = 'RSA-SHA256'
-// const SIGNATURE_ENCODING = 'hex'
const PRIVATE_RSA_KEY_SIZE = 2048
// Password encryption
VIDEO_LANGUAGES,
VIDEO_PRIVACIES,
VIDEO_LICENCES,
- VIDEO_RATE_TYPES
+ VIDEO_RATE_TYPES,
+ VIDEO_MIMETYPE_EXT
}
--- /dev/null
+import * as magnetUtil from 'magnet-uri'
+import * as Sequelize from 'sequelize'
+import { VideoTorrentObject } from '../../../shared'
+import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
+import { database as db } from '../../initializers'
+import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants'
+import { VideoChannelInstance } from '../../models/video/video-channel-interface'
+import { VideoFileAttributes } from '../../models/video/video-file-interface'
+import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
+
+async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelInstance, videoObject: VideoTorrentObject, t: Sequelize.Transaction) {
+ const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoObject.uuid, videoObject.id, t)
+ if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.')
+
+ const duration = videoObject.duration.replace(/[^\d]+/, '')
+ const videoData: VideoAttributes = {
+ name: videoObject.name,
+ uuid: videoObject.uuid,
+ url: videoObject.id,
+ category: parseInt(videoObject.category.identifier, 10),
+ licence: parseInt(videoObject.licence.identifier, 10),
+ language: parseInt(videoObject.language.identifier, 10),
+ nsfw: videoObject.nsfw,
+ description: videoObject.content,
+ channelId: videoChannel.id,
+ duration: parseInt(duration, 10),
+ createdAt: videoObject.published,
+ // FIXME: updatedAt does not seems to be considered by Sequelize
+ updatedAt: videoObject.updated,
+ views: videoObject.views,
+ likes: 0,
+ dislikes: 0,
+ // likes: videoToCreateData.likes,
+ // dislikes: videoToCreateData.dislikes,
+ remote: true,
+ privacy: 1
+ // privacy: videoToCreateData.privacy
+ }
+
+ return videoData
+}
+
+function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
+ const fileUrls = videoObject.url
+ .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1)
+
+ const attributes: VideoFileAttributes[] = []
+ for (const url of fileUrls) {
+ // Fetch associated magnet uri
+ const magnet = videoObject.url
+ .find(u => {
+ return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width
+ })
+ if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url)
+
+ const parsed = magnetUtil.decode(magnet.url)
+ if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
+
+ const attribute = {
+ extname: VIDEO_MIMETYPE_EXT[url.mimeType],
+ infoHash: parsed.infoHash,
+ resolution: url.width,
+ size: url.size,
+ videoId: videoCreated.id
+ }
+ attributes.push(attribute)
+ }
+
+ return attributes
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ videoFileActivityUrlToDBAttributes,
+ videoActivityObjectToDBAttributes
+}
--- /dev/null
+import { VideoTorrentObject } from '../../../shared'
+import { ActivityAdd } from '../../../shared/models/activitypub/activity'
+import { generateThumbnailFromUrl, logger, retryTransactionWrapper, getOrCreateAccount } from '../../helpers'
+import { database as db } from '../../initializers'
+import { AccountInstance } from '../../models/account/account-interface'
+import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+import Bluebird = require('bluebird')
+
+async function processAddActivity (activity: ActivityAdd) {
+ const activityObject = activity.object
+ const activityType = activityObject.type
+ const account = await getOrCreateAccount(activity.actor)
+
+ if (activityType === 'Video') {
+ return processAddVideo(account, activity.id, activityObject as VideoTorrentObject)
+ }
+
+ logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
+ return Promise.resolve(undefined)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ processAddActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
+ const options = {
+ arguments: [ account, videoChannelUrl ,video ],
+ errorMessage: 'Cannot insert the remote video with many retries.'
+ }
+
+ return retryTransactionWrapper(addRemoteVideo, options)
+}
+
+async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) {
+ logger.debug('Adding remote video %s.', videoToCreateData.url)
+
+ await db.sequelize.transaction(async t => {
+ const sequelizeOptions = {
+ transaction: t
+ }
+
+ const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t)
+ if (!videoChannel) throw new Error('Video channel not found.')
+
+ if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
+
+ const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t)
+ const video = db.Video.build(videoData)
+
+ // Don't block on request
+ generateThumbnailFromUrl(video, videoToCreateData.icon)
+ .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
+
+ const videoCreated = await video.save(sequelizeOptions)
+
+ const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
+
+ const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
+ await Promise.all(tasks)
+
+ const tags = videoToCreateData.tag.map(t => t.name)
+ const tagInstances = await db.Tag.findOrCreateTags(tags, t)
+ await videoCreated.setTags(tagInstances, sequelizeOptions)
+ })
+
+ logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
+}
-import {
- ActivityCreate,
- VideoTorrentObject,
- VideoChannelObject
-} from '../../../shared'
+import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared'
+import { ActivityAdd } from '../../../shared/models/activitypub/activity'
+import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers'
import { database as db } from '../../initializers'
-import { logger, retryTransactionWrapper } from '../../helpers'
+import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+import Bluebird = require('bluebird')
+import { AccountInstance } from '../../models/account/account-interface'
+import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
-function processCreateActivity (activity: ActivityCreate) {
+async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object
const activityType = activityObject.type
+ const account = await getOrCreateAccount(activity.actor)
- if (activityType === 'Video') {
- return processCreateVideo(activityObject as VideoTorrentObject)
- } else if (activityType === 'VideoChannel') {
- return processCreateVideoChannel(activityObject as VideoChannelObject)
+ if (activityType === 'VideoChannel') {
+ return processCreateVideoChannel(account, activityObject as VideoChannelObject)
}
logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
- return Promise.resolve()
+ return Promise.resolve(undefined)
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-function processCreateVideo (video: VideoTorrentObject) {
+function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
const options = {
- arguments: [ video ],
- errorMessage: 'Cannot insert the remote video with many retries.'
+ arguments: [ account, videoChannelToCreateData ],
+ errorMessage: 'Cannot insert the remote video channel with many retries.'
}
- return retryTransactionWrapper(addRemoteVideo, options)
+ return retryTransactionWrapper(addRemoteVideoChannel, options)
}
-async function addRemoteVideo (videoToCreateData: VideoTorrentObject) {
- logger.debug('Adding remote video %s.', videoToCreateData.url)
+async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
+ logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
await db.sequelize.transaction(async t => {
- const sequelizeOptions = {
- transaction: t
- }
-
- const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid)
- if (videoFromDatabase) throw new Error('UUID already exists.')
-
- const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
- if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
-
- const tags = videoToCreateData.tags
- const tagInstances = await db.Tag.findOrCreateTags(tags, t)
-
- const videoData = {
- name: videoToCreateData.name,
- uuid: videoToCreateData.uuid,
- category: videoToCreateData.category,
- licence: videoToCreateData.licence,
- language: videoToCreateData.language,
- nsfw: videoToCreateData.nsfw,
- description: videoToCreateData.truncatedDescription,
- channelId: videoChannel.id,
- duration: videoToCreateData.duration,
- createdAt: videoToCreateData.createdAt,
- // FIXME: updatedAt does not seems to be considered by Sequelize
- updatedAt: videoToCreateData.updatedAt,
- views: videoToCreateData.views,
- likes: videoToCreateData.likes,
- dislikes: videoToCreateData.dislikes,
+ let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
+ if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
+
+ const videoChannelData = {
+ name: videoChannelToCreateData.name,
+ description: videoChannelToCreateData.content,
+ uuid: videoChannelToCreateData.uuid,
+ createdAt: videoChannelToCreateData.published,
+ updatedAt: videoChannelToCreateData.updated,
remote: true,
- privacy: videoToCreateData.privacy
- }
-
- const video = db.Video.build(videoData)
- await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
- const videoCreated = await video.save(sequelizeOptions)
-
- const tasks = []
- for (const fileData of videoToCreateData.files) {
- const videoFileInstance = db.VideoFile.build({
- extname: fileData.extname,
- infoHash: fileData.infoHash,
- resolution: fileData.resolution,
- size: fileData.size,
- videoId: videoCreated.id
- })
-
- tasks.push(videoFileInstance.save(sequelizeOptions))
+ accountId: account.id
}
- await Promise.all(tasks)
+ videoChannel = db.VideoChannel.build(videoChannelData)
+ videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
- await videoCreated.setTags(tagInstances, sequelizeOptions)
+ await videoChannel.save({ transaction: t })
})
- logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
-}
-
-function processCreateVideoChannel (videoChannel: VideoChannelObject) {
-
+ logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
}
-import {
- ActivityCreate,
- VideoTorrentObject,
- VideoChannelObject
-} from '../../../shared'
+import { VideoChannelObject, VideoTorrentObject } from '../../../shared'
+import { ActivityUpdate } from '../../../shared/models/activitypub/activity'
+import { getOrCreateAccount } from '../../helpers/activitypub'
+import { retryTransactionWrapper } from '../../helpers/database-utils'
+import { logger } from '../../helpers/logger'
+import { resetSequelizeInstance } from '../../helpers/utils'
+import { database as db } from '../../initializers'
+import { AccountInstance } from '../../models/account/account-interface'
+import { VideoInstance } from '../../models/video/video-interface'
+import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
+import Bluebird = require('bluebird')
+
+async function processUpdateActivity (activity: ActivityUpdate) {
+ const account = await getOrCreateAccount(activity.actor)
-function processUpdateActivity (activity: ActivityCreate) {
if (activity.object.type === 'Video') {
- return processUpdateVideo(activity.object)
+ return processUpdateVideo(account, activity.object)
} else if (activity.object.type === 'VideoChannel') {
- return processUpdateVideoChannel(activity.object)
+ return processUpdateVideoChannel(account, activity.object)
}
+
+ return undefined
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-function processUpdateVideo (video: VideoTorrentObject) {
+function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
+ const options = {
+ arguments: [ account, video ],
+ errorMessage: 'Cannot update the remote video with many retries'
+ }
+ return retryTransactionWrapper(updateRemoteVideo, options)
}
-function processUpdateVideoChannel (videoChannel: VideoChannelObject) {
+async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) {
+ logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
+ let videoInstance: VideoInstance
+ let videoFieldsSave: object
+
+ try {
+ await db.sequelize.transaction(async t => {
+ const sequelizeOptions = {
+ transaction: t
+ }
+
+ const videoInstance = await db.Video.loadByUrl(videoAttributesToUpdate.id, t)
+ if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
+
+ if (videoInstance.VideoChannel.Account.id !== account.id) {
+ throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
+ }
+
+ const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate, t)
+ videoInstance.set('name', videoData.name)
+ videoInstance.set('category', videoData.category)
+ videoInstance.set('licence', videoData.licence)
+ videoInstance.set('language', videoData.language)
+ videoInstance.set('nsfw', videoData.nsfw)
+ videoInstance.set('description', videoData.description)
+ videoInstance.set('duration', videoData.duration)
+ videoInstance.set('createdAt', videoData.createdAt)
+ videoInstance.set('updatedAt', videoData.updatedAt)
+ videoInstance.set('views', videoData.views)
+ // videoInstance.set('likes', videoData.likes)
+ // videoInstance.set('dislikes', videoData.dislikes)
+ // videoInstance.set('privacy', videoData.privacy)
+
+ await videoInstance.save(sequelizeOptions)
+
+ // Remove old video files
+ const videoFileDestroyTasks: Bluebird<void>[] = []
+ for (const videoFile of videoInstance.VideoFiles) {
+ videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
+ }
+ await Promise.all(videoFileDestroyTasks)
+
+ const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
+ const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
+ await Promise.all(tasks)
+
+ const tags = videoAttributesToUpdate.tag.map(t => t.name)
+ const tagInstances = await db.Tag.findOrCreateTags(tags, t)
+ await videoInstance.setTags(tagInstances, sequelizeOptions)
+ })
+
+ logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
+ } catch (err) {
+ if (videoInstance !== undefined && videoFieldsSave !== undefined) {
+ resetSequelizeInstance(videoInstance, videoFieldsSave)
+ }
+
+ // This is just a debug because we will retry the insert
+ logger.debug('Cannot update the remote video.', err)
+ throw err
+ }
+}
+
+async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
+ const options = {
+ arguments: [ account, videoChannel ],
+ errorMessage: 'Cannot update the remote video channel with many retries.'
+ }
+
+ await retryTransactionWrapper(updateRemoteVideoChannel, options)
+}
+
+async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
+ logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
+
+ await db.sequelize.transaction(async t => {
+ const sequelizeOptions = { transaction: t }
+
+ const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
+ if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
+
+ if (videoChannelInstance.Account.id !== account.id) {
+ throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
+ }
+
+ videoChannelInstance.set('name', videoChannel.name)
+ videoChannelInstance.set('description', videoChannel.content)
+ videoChannelInstance.set('createdAt', videoChannel.published)
+ videoChannelInstance.set('updatedAt', videoChannel.updated)
+
+ await videoChannelInstance.save(sequelizeOptions)
+ })
+ logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
}
--- /dev/null
+import { body } from 'express-validator/check'
+import * as express from 'express'
+
+import { logger, isRootActivityValid } from '../../../helpers'
+import { checkErrors } from '../utils'
+
+const activityPubValidator = [
+ body('data').custom(isRootActivityValid),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking activity pub parameters', { parameters: req.body })
+
+ checkErrors(req, res, next)
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ activityPubValidator
+}
+++ /dev/null
-import { body } from 'express-validator/check'
-import * as express from 'express'
-
-import {
- logger,
- isArray,
- removeBadRequestVideos,
- removeBadRequestVideosQadu,
- removeBadRequestVideosEvents
-} from '../../../helpers'
-import { checkErrors } from '../utils'
-
-const remoteVideosValidator = [
- body('data').custom(isArray),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking remoteVideos parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- removeBadRequestVideos(req.body.data)
-
- return next()
- })
- }
-]
-
-const remoteQaduVideosValidator = [
- body('data').custom(isArray),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking remoteQaduVideos parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- removeBadRequestVideosQadu(req.body.data)
-
- return next()
- })
- }
-]
-
-const remoteEventsVideosValidator = [
- body('data').custom(isArray),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking remoteEventsVideos parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- removeBadRequestVideosEvents(req.body.data)
-
- return next()
- })
- }
-]
-
-// ---------------------------------------------------------------------------
-
-export {
- remoteVideosValidator,
- remoteQaduVideosValidator,
- remoteEventsVideosValidator
-}
export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
+ export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
+ export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
}
export interface VideoChannelClass {
loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
+ loadByUrl: VideoChannelMethods.LoadByUrl
+ loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
}
export interface VideoChannelAttributes {
name: string
description: string
remote: boolean
- url: string
+ url?: string
Account?: AccountInstance
Videos?: VideoInstance[]
let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
+let loadByUrl: VideoChannelMethods.LoadByUrl
+let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
loadByUUID,
loadByHostAndUUID,
loadAndPopulateAccountAndVideos,
- countByAccount
+ countByAccount,
+ loadByUrl,
+ loadByUUIDOrUrl
]
const instanceMethods = [
isOwned,
toFormattedJSON,
- toActivityPubObject,
+ toActivityPubObject
]
addMethodsToModel(VideoChannel, classMethods, instanceMethods)
return VideoChannel.findOne(query)
}
+loadByUrl = function (url: string, t?: Sequelize.Transaction) {
+ const query: Sequelize.FindOptions<VideoChannelAttributes> = {
+ where: {
+ url
+ }
+ }
+
+ if (t !== undefined) query.transaction = t
+
+ return VideoChannel.findOne(query)
+}
+
+loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) {
+ const query: Sequelize.FindOptions<VideoChannelAttributes> = {
+ where: {
+ [Sequelize.Op.or]: [
+ { uuid },
+ { url }
+ ]
+ },
+ }
+
+ if (t !== undefined) query.transaction = t
+
+ return VideoChannel.findOne(query)
+}
+
loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
const query: Sequelize.FindOptions<VideoChannelAttributes> = {
where: {
export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
+ export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
export type RemovePreview = (this: VideoInstance) => Promise<void>
loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
loadByUUID: VideoMethods.LoadByUUID
loadByUrl: VideoMethods.LoadByUrl
+ loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
likes?: number
dislikes?: number
remote: boolean
- url: string
+ url?: string
+
+ createdAt?: Date
+ updatedAt?: Date
parentId?: number
channelId?: number
}
export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
- createdAt: Date
- updatedAt: Date
-
createPreview: VideoMethods.CreatePreview
createThumbnail: VideoMethods.CreateThumbnail
createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
}
export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
-
statPromise,
generateImageFromVideoFile,
transcode,
- getVideoFileHeight
+ getVideoFileHeight,
+ getActivityPubUrl
} from '../../helpers'
import {
CONFIG,
let listOwnedByAccount: VideoMethods.ListOwnedByAccount
let load: VideoMethods.Load
let loadByUUID: VideoMethods.LoadByUUID
-let loadByUrl: VideoMethods.LoadByUrl
+let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
loadAndPopulateAccount,
loadAndPopulateAccountAndPodAndTags,
loadByHostAndUUID,
+ loadByUUIDOrURL,
loadByUUID,
loadLocalVideoByUUID,
loadByUUIDAndPopulateAccountAndPodAndTags,
const videoObject: VideoTorrentObject = {
type: 'Video',
+ id: getActivityPubUrl('video', this.uuid),
name: this.name,
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
duration: 'PT' + this.duration + 'S',
getLicenceLabel = function (this: VideoInstance) {
let licenceLabel = VIDEO_LICENCES[this.licence]
+
// Maybe our pod is not up to date and there are new licences since our version
if (!licenceLabel) licenceLabel = 'Unknown'
return Video.findOne(query)
}
+loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
+ const query: Sequelize.FindOptions<VideoAttributes> = {
+ where: {
+ [Sequelize.Op.or]: [
+ { uuid },
+ { url }
+ ]
+ },
+ include: [ Video['sequelize'].models.VideoFile ]
+ }
+
+ if (t !== undefined) query.transaction = t
+
+ return Video.findOne(query)
+}
+
loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
const query: Sequelize.FindOptions<VideoAttributes> = {
where: {
export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag
// Flag -> report abuse
-export type ActivityType = 'Create' | 'Update' | 'Flag'
+export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag'
export interface BaseActivity {
'@context'?: any[]
export interface ActivityCreate extends BaseActivity {
type: 'Create'
- object: VideoTorrentObject | VideoChannelObject
+ object: VideoChannelObject
+}
+
+export interface ActivityAdd extends BaseActivity {
+ type: 'Add'
+ object: VideoTorrentObject
}
export interface ActivityUpdate extends BaseActivity {
export interface VideoChannelObject {
type: 'VideoChannel'
+ id: string
name: string
content: string
- uuid: ActivityIdentifierObject
+ uuid: string
+ published: Date
+ updated: Date
}
export interface VideoTorrentObject {
type: 'Video'
+ id: string
name: string
duration: string
uuid: string