"express-validator": "^4.1.1",
"fluent-ffmpeg": "^2.1.0",
"js-yaml": "^3.5.4",
+ "jsonld": "^0.4.12",
+ "jsonld-signatures": "^1.2.1",
"lodash": "^4.11.1",
"magnet-uri": "^5.1.4",
"mkdirp": "^0.5.1",
"morgan": "^1.5.3",
"multer": "^1.1.0",
- "openssl-wrapper": "^0.3.4",
"parse-torrent": "^5.8.0",
"password-generator": "^2.0.2",
+ "pem": "^1.12.3",
"pg": "^6.4.2",
"pg-hstore": "^2.3.2",
"request": "^2.81.0",
"typescript": "^2.5.2",
"uuid": "^3.1.0",
"validator": "^9.0.0",
+ "webfinger.js": "^2.6.6",
"winston": "^2.1.1",
"ws": "^3.1.0"
},
"@types/morgan": "^1.7.32",
"@types/multer": "^1.3.3",
"@types/node": "^8.0.3",
+ "@types/pem": "^1.9.3",
"@types/request": "^2.0.3",
"@types/sequelize": "^4.0.55",
"@types/supertest": "^2.0.3",
--- /dev/null
+// Intercept ActivityPub client requests
+import * as express from 'express'
+
+import { database as db } from '../../initializers'
+import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
+import { pageToStartAndCount } from '../../helpers'
+import { AccountInstance } from '../../models'
+import { activityPubCollectionPagination } from '../../helpers/activitypub'
+import { ACTIVITY_PUB } from '../../initializers/constants'
+import { asyncMiddleware } from '../../middlewares/async'
+
+const activityPubClientRouter = express.Router()
+
+activityPubClientRouter.get('/account/:name',
+ executeIfActivityPub(localAccountValidator),
+ executeIfActivityPub(asyncMiddleware(accountController))
+)
+
+activityPubClientRouter.get('/account/:name/followers',
+ executeIfActivityPub(localAccountValidator),
+ executeIfActivityPub(asyncMiddleware(accountFollowersController))
+)
+
+activityPubClientRouter.get('/account/:name/following',
+ executeIfActivityPub(localAccountValidator),
+ executeIfActivityPub(asyncMiddleware(accountFollowingController))
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ activityPubClientRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function accountController (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const account: AccountInstance = res.locals.account
+
+ return res.json(account.toActivityPubObject()).end()
+}
+
+async function accountFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const account: AccountInstance = res.locals.account
+
+ const page = req.params.page || 1
+ const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
+
+ const result = await db.Account.listFollowerUrlsForApi(account.name, start, count)
+ const activityPubResult = activityPubCollectionPagination(req.url, page, result)
+
+ return res.json(activityPubResult)
+}
+
+async function accountFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const account: AccountInstance = res.locals.account
+
+ const page = req.params.page || 1
+ const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
+
+ const result = await db.Account.listFollowingUrlsForApi(account.name, start, count)
+ const activityPubResult = activityPubCollectionPagination(req.url, page, result)
+
+ return res.json(activityPubResult)
+}
--- /dev/null
+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 { logger } from '../../helpers'
+
+const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
+ Create: processCreateActivity,
+ Update: processUpdateActivity,
+ Flag: processFlagActivity
+}
+
+const inboxRouter = express.Router()
+
+inboxRouter.post('/',
+ signatureValidator,
+ asyncMiddleware(checkSignature),
+ // inboxValidator,
+ asyncMiddleware(inboxController)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ inboxRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const rootActivity: RootActivity = req.body
+ let activities: Activity[] = []
+
+ if ([ 'Collection', 'CollectionPage' ].indexOf(rootActivity.type) !== -1) {
+ activities = (rootActivity as ActivityPubCollection).items
+ } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].indexOf(rootActivity.type) !== -1) {
+ activities = (rootActivity as ActivityPubOrderedCollection).orderedItems
+ } else {
+ activities = [ rootActivity as Activity ]
+ }
+
+ await processActivities(activities)
+
+ res.status(204).end()
+}
+
+async function processActivities (activities: Activity[]) {
+ for (const activity of activities) {
+ const activityProcessor = processActivity[activity.type]
+ if (activityProcessor === undefined) {
+ logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id })
+ continue
+ }
+
+ await activityProcessor(activity)
+ }
+}
--- /dev/null
+import * as express from 'express'
+
+import { badRequest } from '../../helpers'
+import { inboxRouter } from './inbox'
+
+const remoteRouter = express.Router()
+
+remoteRouter.use('/inbox', inboxRouter)
+remoteRouter.use('/*', badRequest)
+
+// ---------------------------------------------------------------------------
+
+export {
+ remoteRouter
+}
--- /dev/null
+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()
+}
--- /dev/null
+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
+ }
+}
+++ /dev/null
-import * as express from 'express'
-
-import { badRequest } from '../../../helpers'
-
-import { remotePodsRouter } from './pods'
-import { remoteVideosRouter } from './videos'
-
-const remoteRouter = express.Router()
-
-remoteRouter.use('/pods', remotePodsRouter)
-remoteRouter.use('/videos', remoteVideosRouter)
-remoteRouter.use('/*', badRequest)
-
-// ---------------------------------------------------------------------------
-
-export {
- remoteRouter
-}
+++ /dev/null
-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()
-}
+++ /dev/null
-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
- }
-}
--- /dev/null
+import * as url from 'url'
+
+import { database as db } from '../initializers'
+import { logger } from './logger'
+import { doRequest } from './requests'
+import { isRemoteAccountValid } from './custom-validators'
+import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
+import { ResultList } from '../../shared/models/result-list.model'
+
+async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
+ const options = {
+ uri: accountUrl,
+ method: 'GET'
+ }
+
+ let requestResult
+ try {
+ requestResult = await doRequest(options)
+ } catch (err) {
+ logger.warning('Cannot fetch remote account %s.', accountUrl, err)
+ return undefined
+ }
+
+ const accountJSON: ActivityPubActor = requestResult.body
+ if (isRemoteAccountValid(accountJSON) === false) return undefined
+
+ const followersCount = await fetchAccountCount(accountJSON.followers)
+ const followingCount = await fetchAccountCount(accountJSON.following)
+
+ const account = db.Account.build({
+ uuid: accountJSON.uuid,
+ name: accountJSON.preferredUsername,
+ url: accountJSON.url,
+ publicKey: accountJSON.publicKey.publicKeyPem,
+ privateKey: null,
+ followersCount: followersCount,
+ followingCount: followingCount,
+ inboxUrl: accountJSON.inbox,
+ outboxUrl: accountJSON.outbox,
+ sharedInboxUrl: accountJSON.endpoints.sharedInbox,
+ followersUrl: accountJSON.followers,
+ followingUrl: accountJSON.following
+ })
+
+ const accountHost = url.parse(account.url).host
+ const podOptions = {
+ where: {
+ host: accountHost
+ },
+ defaults: {
+ host: accountHost
+ }
+ }
+ const pod = await db.Pod.findOrCreate(podOptions)
+
+ return { account, pod }
+}
+
+function activityPubContextify (data: object) {
+ return Object.assign(data,{
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ 'https://w3id.org/security/v1',
+ {
+ 'Hashtag': 'as:Hashtag',
+ 'uuid': 'http://schema.org/identifier',
+ 'category': 'http://schema.org/category',
+ 'licence': 'http://schema.org/license',
+ 'nsfw': 'as:sensitive',
+ 'language': 'http://schema.org/inLanguage',
+ 'views': 'http://schema.org/Number',
+ 'size': 'http://schema.org/Number'
+ }
+ ]
+ })
+}
+
+function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
+ const baseUrl = url.split('?').shift
+
+ const obj = {
+ id: baseUrl,
+ type: 'Collection',
+ totalItems: result.total,
+ first: {
+ id: baseUrl + '?page=' + page,
+ type: 'CollectionPage',
+ totalItems: result.total,
+ next: baseUrl + '?page=' + (page + 1),
+ partOf: baseUrl,
+ items: result.data
+ }
+ }
+
+ return activityPubContextify(obj)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ fetchRemoteAccountAndCreatePod,
+ activityPubContextify,
+ activityPubCollectionPagination
+}
+
+// ---------------------------------------------------------------------------
+
+async function fetchAccountCount (url: string) {
+ const options = {
+ uri: url,
+ method: 'GET'
+ }
+
+ let requestResult
+ try {
+ requestResult = await doRequest(options)
+ } catch (err) {
+ logger.warning('Cannot fetch remote account count %s.', url, err)
+ return undefined
+ }
+
+ return requestResult.totalItems ? requestResult.totalItems : 0
+}
import * as bcrypt from 'bcrypt'
import * as createTorrent from 'create-torrent'
import * as rimraf from 'rimraf'
-import * as openssl from 'openssl-wrapper'
-import * as Promise from 'bluebird'
+import * as pem from 'pem'
+import * as jsonld from 'jsonld'
+import * as jsig from 'jsonld-signatures'
+jsig.use('jsonld', jsonld)
function isTestInstance () {
return process.env.NODE_ENV === 'test'
return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
}
+function pageToStartAndCount (page: number, itemsPerPage: number) {
+ const start = (page - 1) * itemsPerPage
+
+ return { start, count: itemsPerPage }
+}
+
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
return function promisified (): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
const mkdirpPromise = promisify1<string, string>(mkdirp)
const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
const accessPromise = promisify1WithVoid<string | Buffer>(access)
-const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec)
+const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
+const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
const rimrafPromise = promisify1WithVoid<string>(rimraf)
const statPromise = promisify1<string, Stats>(stat)
+const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign)
+const jsonldVerifyPromise = promisify2<object, object, object>(jsig.verify)
// ---------------------------------------------------------------------------
isTestInstance,
root,
escapeHTML,
+ pageToStartAndCount,
promisify0,
promisify1,
+
readdirPromise,
readFilePromise,
readFileBufferPromise,
mkdirpPromise,
pseudoRandomBytesPromise,
accessPromise,
- opensslExecPromise,
+ createPrivateKey,
+ getPublicKey,
bcryptComparePromise,
bcryptGenSaltPromise,
bcryptHashPromise,
createTorrentPromise,
rimrafPromise,
- statPromise
+ statPromise,
+ jsonldSignPromise,
+ jsonldVerifyPromise
}
--- /dev/null
+import * as validator from 'validator'
+
+import { exists, isUUIDValid } from '../misc'
+import { isActivityPubUrlValid } from './misc'
+import { isUserUsernameValid } from '../users'
+
+function isAccountEndpointsObjectValid (endpointObject: any) {
+ return isAccountSharedInboxValid(endpointObject.sharedInbox)
+}
+
+function isAccountSharedInboxValid (sharedInbox: string) {
+ return isActivityPubUrlValid(sharedInbox)
+}
+
+function isAccountPublicKeyObjectValid (publicKeyObject: any) {
+ return isAccountPublicKeyIdValid(publicKeyObject.id) &&
+ isAccountPublicKeyOwnerValid(publicKeyObject.owner) &&
+ isAccountPublicKeyValid(publicKeyObject.publicKeyPem)
+}
+
+function isAccountPublicKeyIdValid (id: string) {
+ return isActivityPubUrlValid(id)
+}
+
+function isAccountTypeValid (type: string) {
+ return type === 'Person' || type === 'Application'
+}
+
+function isAccountPublicKeyOwnerValid (owner: string) {
+ return isActivityPubUrlValid(owner)
+}
+
+function isAccountPublicKeyValid (publicKey: string) {
+ return exists(publicKey) &&
+ typeof publicKey === 'string' &&
+ publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
+ publicKey.endsWith('-----END PUBLIC KEY-----')
+}
+
+function isAccountIdValid (id: string) {
+ return isActivityPubUrlValid(id)
+}
+
+function isAccountFollowingValid (id: string) {
+ return isActivityPubUrlValid(id)
+}
+
+function isAccountFollowersValid (id: string) {
+ return isActivityPubUrlValid(id)
+}
+
+function isAccountInboxValid (inbox: string) {
+ return isActivityPubUrlValid(inbox)
+}
+
+function isAccountOutboxValid (outbox: string) {
+ return isActivityPubUrlValid(outbox)
+}
+
+function isAccountNameValid (name: string) {
+ return isUserUsernameValid(name)
+}
+
+function isAccountPreferredUsernameValid (preferredUsername: string) {
+ return isAccountNameValid(preferredUsername)
+}
+
+function isAccountUrlValid (url: string) {
+ return isActivityPubUrlValid(url)
+}
+
+function isAccountPrivateKeyValid (privateKey: string) {
+ return exists(privateKey) &&
+ typeof privateKey === 'string' &&
+ privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
+ privateKey.endsWith('-----END RSA PRIVATE KEY-----')
+}
+
+function isRemoteAccountValid (remoteAccount: any) {
+ return isAccountIdValid(remoteAccount.id) &&
+ isUUIDValid(remoteAccount.uuid) &&
+ isAccountTypeValid(remoteAccount.type) &&
+ isAccountFollowingValid(remoteAccount.following) &&
+ isAccountFollowersValid(remoteAccount.followers) &&
+ isAccountInboxValid(remoteAccount.inbox) &&
+ isAccountOutboxValid(remoteAccount.outbox) &&
+ isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
+ isAccountUrlValid(remoteAccount.url) &&
+ isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
+ isAccountEndpointsObjectValid(remoteAccount.endpoint)
+}
+
+function isAccountFollowingCountValid (value: string) {
+ return exists(value) && validator.isInt('' + value, { min: 0 })
+}
+
+function isAccountFollowersCountValid (value: string) {
+ return exists(value) && validator.isInt('' + value, { min: 0 })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isAccountEndpointsObjectValid,
+ isAccountSharedInboxValid,
+ isAccountPublicKeyObjectValid,
+ isAccountPublicKeyIdValid,
+ isAccountTypeValid,
+ isAccountPublicKeyOwnerValid,
+ isAccountPublicKeyValid,
+ isAccountIdValid,
+ isAccountFollowingValid,
+ isAccountFollowersValid,
+ isAccountInboxValid,
+ isAccountOutboxValid,
+ isAccountPreferredUsernameValid,
+ isAccountUrlValid,
+ isAccountPrivateKeyValid,
+ isRemoteAccountValid,
+ isAccountFollowingCountValid,
+ isAccountFollowersCountValid,
+ isAccountNameValid
+}
--- /dev/null
+export * from './account'
+export * from './signature'
+export * from './misc'
+export * from './videos'
--- /dev/null
+import { exists } from '../misc'
+
+function isActivityPubUrlValid (url: string) {
+ const isURLOptions = {
+ require_host: true,
+ require_tld: true,
+ require_protocol: true,
+ require_valid_protocol: true,
+ protocols: [ 'http', 'https' ]
+ }
+
+ return exists(url) && validator.isURL(url, isURLOptions)
+}
+
+export {
+ isActivityPubUrlValid
+}
--- /dev/null
+import { exists } from '../misc'
+import { isActivityPubUrlValid } from './misc'
+
+function isSignatureTypeValid (signatureType: string) {
+ return exists(signatureType) && signatureType === 'GraphSignature2012'
+}
+
+function isSignatureCreatorValid (signatureCreator: string) {
+ return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator)
+}
+
+function isSignatureValueValid (signatureValue: string) {
+ return exists(signatureValue) && signatureValue.length > 0
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isSignatureTypeValid,
+ isSignatureCreatorValid,
+ isSignatureValueValid
+}
--- /dev/null
+import 'express-validator'
+import { has, values } from 'lodash'
+
+import {
+ REQUEST_ENDPOINTS,
+ REQUEST_ENDPOINT_ACTIONS,
+ REQUEST_VIDEO_EVENT_TYPES
+} from '../../../initializers'
+import { isArray, isDateValid, isUUIDValid } from '../misc'
+import {
+ isVideoThumbnailDataValid,
+ isVideoAbuseReasonValid,
+ isVideoAbuseReporterUsernameValid,
+ isVideoViewsValid,
+ isVideoLikesValid,
+ isVideoDislikesValid,
+ isVideoEventCountValid,
+ isRemoteVideoCategoryValid,
+ isRemoteVideoLicenceValid,
+ isRemoteVideoLanguageValid,
+ isVideoNSFWValid,
+ isVideoTruncatedDescriptionValid,
+ isVideoDurationValid,
+ isVideoFileInfoHashValid,
+ isVideoNameValid,
+ isVideoTagsValid,
+ 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 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)
+ }
+ }
+}
+
+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 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)
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ removeBadRequestVideos,
+ removeBadRequestVideosQadu,
+ removeBadRequestVideosEvents
+}
+
+// ---------------------------------------------------------------------------
+
+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 checkAddVideo (video: any) {
+ return isCommonVideoAttributesValid(video) &&
+ isUUIDValid(video.channelUUID) &&
+ isVideoThumbnailDataValid(video.thumbnailData)
+}
+
+function checkUpdateVideo (video: any) {
+ return isCommonVideoAttributesValid(video)
+}
+
+function checkRemoveVideo (video: any) {
+ return isUUIDValid(video.uuid)
+}
+
+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)
+}
-export * from './remote'
+export * from './activitypub'
export * from './misc'
export * from './pods'
export * from './pods'
+++ /dev/null
-export * from './videos'
+++ /dev/null
-import 'express-validator'
-import { has, values } from 'lodash'
-
-import {
- REQUEST_ENDPOINTS,
- REQUEST_ENDPOINT_ACTIONS,
- REQUEST_VIDEO_EVENT_TYPES
-} from '../../../initializers'
-import { isArray, isDateValid, isUUIDValid } from '../misc'
-import {
- isVideoThumbnailDataValid,
- isVideoAbuseReasonValid,
- isVideoAbuseReporterUsernameValid,
- isVideoViewsValid,
- isVideoLikesValid,
- isVideoDislikesValid,
- isVideoEventCountValid,
- isRemoteVideoCategoryValid,
- isRemoteVideoLicenceValid,
- isRemoteVideoLanguageValid,
- isVideoNSFWValid,
- isVideoTruncatedDescriptionValid,
- isVideoDurationValid,
- isVideoFileInfoHashValid,
- isVideoNameValid,
- isVideoTagsValid,
- 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 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)
- }
- }
-}
-
-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 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)
- }
- }
-}
-
-// ---------------------------------------------------------------------------
-
-export {
- removeBadRequestVideos,
- removeBadRequestVideosQadu,
- removeBadRequestVideosEvents
-}
-
-// ---------------------------------------------------------------------------
-
-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 checkAddVideo (video: any) {
- return isCommonVideoAttributesValid(video) &&
- isUUIDValid(video.channelUUID) &&
- isVideoThumbnailDataValid(video.thumbnailData)
-}
-
-function checkUpdateVideo (video: any) {
- return isCommonVideoAttributesValid(video)
-}
-
-function checkRemoveVideo (video: any) {
- return isUUIDValid(video.uuid)
-}
-
-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)
-}
-import * as Promise from 'bluebird'
import * as ffmpeg from 'fluent-ffmpeg'
import { CONFIG } from '../initializers'
+export * from './activitypub'
export * from './core-utils'
export * from './logger'
export * from './custom-validators'
export * from './peertube-crypto'
export * from './requests'
export * from './utils'
+export * from './webfinger'
-import * as crypto from 'crypto'
-import { join } from 'path'
+import * as jsig from 'jsonld-signatures'
import {
- SIGNATURE_ALGORITHM,
- SIGNATURE_ENCODING,
- PRIVATE_CERT_NAME,
- CONFIG,
- BCRYPT_SALT_SIZE,
- PUBLIC_CERT_NAME
+ PRIVATE_RSA_KEY_SIZE,
+ BCRYPT_SALT_SIZE
} from '../initializers'
import {
- readFilePromise,
bcryptComparePromise,
bcryptGenSaltPromise,
bcryptHashPromise,
- accessPromise,
- opensslExecPromise
+ createPrivateKey,
+ getPublicKey,
+ jsonldSignPromise,
+ jsonldVerifyPromise
} from './core-utils'
import { logger } from './logger'
+import { AccountInstance } from '../models/account/account-interface'
-function checkSignature (publicKey: string, data: string, hexSignature: string) {
- const verify = crypto.createVerify(SIGNATURE_ALGORITHM)
-
- let dataString
- if (typeof data === 'string') {
- dataString = data
- } else {
- try {
- dataString = JSON.stringify(data)
- } catch (err) {
- logger.error('Cannot check signature.', err)
- return false
- }
- }
+async function createPrivateAndPublicKeys () {
+ logger.info('Generating a RSA key...')
- verify.update(dataString, 'utf8')
+ const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE)
+ const { publicKey } = await getPublicKey(key)
- const isValid = verify.verify(publicKey, hexSignature, SIGNATURE_ENCODING)
- return isValid
+ return { privateKey: key, publicKey }
}
-async function sign (data: string | Object) {
- const sign = crypto.createSign(SIGNATURE_ALGORITHM)
-
- let dataString: string
- if (typeof data === 'string') {
- dataString = data
- } else {
- try {
- dataString = JSON.stringify(data)
- } catch (err) {
- logger.error('Cannot sign data.', err)
- return ''
- }
+function isSignatureVerified (fromAccount: AccountInstance, signedDocument: object) {
+ const publicKeyObject = {
+ '@context': jsig.SECURITY_CONTEXT_URL,
+ '@id': fromAccount.url,
+ '@type': 'CryptographicKey',
+ owner: fromAccount.url,
+ publicKeyPem: fromAccount.publicKey
}
- sign.update(dataString, 'utf8')
+ const publicKeyOwnerObject = {
+ '@context': jsig.SECURITY_CONTEXT_URL,
+ '@id': fromAccount.url,
+ publicKey: [ publicKeyObject ]
+ }
- const myKey = await getMyPrivateCert()
- return sign.sign(myKey, SIGNATURE_ENCODING)
-}
+ const options = {
+ publicKey: publicKeyObject,
+ publicKeyOwner: publicKeyOwnerObject
+ }
-function comparePassword (plainPassword: string, hashPassword: string) {
- return bcryptComparePromise(plainPassword, hashPassword)
+ return jsonldVerifyPromise(signedDocument, options)
+ .catch(err => {
+ logger.error('Cannot check signature.', err)
+ return false
+ })
}
-async function createCertsIfNotExist () {
- const exist = await certsExist()
- if (exist === true) {
- return
+function signObject (byAccount: AccountInstance, data: any) {
+ const options = {
+ privateKeyPem: byAccount.privateKey,
+ creator: byAccount.url
}
- return createCerts()
+ return jsonldSignPromise(data, options)
+}
+
+function comparePassword (plainPassword: string, hashPassword: string) {
+ return bcryptComparePromise(plainPassword, hashPassword)
}
async function cryptPassword (password: string) {
return bcryptHashPromise(password, salt)
}
-function getMyPrivateCert () {
- const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
- return readFilePromise(certPath, 'utf8')
-}
-
-function getMyPublicCert () {
- const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME)
- return readFilePromise(certPath, 'utf8')
-}
-
// ---------------------------------------------------------------------------
export {
- checkSignature,
+ isSignatureVerified,
comparePassword,
- createCertsIfNotExist,
+ createPrivateAndPublicKeys,
cryptPassword,
- getMyPrivateCert,
- getMyPublicCert,
- sign
-}
-
-// ---------------------------------------------------------------------------
-
-async function certsExist () {
- const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
-
- // If there is an error the certificates do not exist
- try {
- await accessPromise(certPath)
-
- return true
- } catch {
- return false
- }
-}
-
-async function createCerts () {
- const exist = await certsExist()
- if (exist === true) {
- const errorMessage = 'Certs already exist.'
- logger.warning(errorMessage)
- throw new Error(errorMessage)
- }
-
- logger.info('Generating a RSA key...')
-
- const privateCertPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
- const genRsaOptions = {
- 'out': privateCertPath,
- '2048': false
- }
-
- await opensslExecPromise('genrsa', genRsaOptions)
- logger.info('RSA key generated.')
- logger.info('Managing public key...')
-
- const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub')
- const rsaOptions = {
- 'in': privateCertPath,
- 'pubout': true,
- 'out': publicCertPath
- }
-
- await opensslExecPromise('rsa', rsaOptions)
+ signObject
}
} from '../initializers'
import { PodInstance } from '../models'
import { PodSignature } from '../../shared'
-import { sign } from './peertube-crypto'
+import { signObject } from './peertube-crypto'
+
+function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
+ return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
+ request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
+ })
+}
type MakeRetryRequestParams = {
url: string,
}
type MakeSecureRequestParams = {
- method: 'GET' | 'POST'
toPod: PodInstance
path: string
data?: Object
}
function makeSecureRequest (params: MakeSecureRequestParams) {
- return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
- const requestParams: {
- url: string,
- json: {
- signature: PodSignature,
- data: any
- }
- } = {
- url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
- json: {
- signature: null,
- data: null
- }
+ const requestParams: {
+ method: 'POST',
+ uri: string,
+ json: {
+ signature: PodSignature,
+ data: any
}
-
- if (params.method !== 'POST') {
- return rej(new Error('Cannot make a secure request with a non POST method.'))
+ } = {
+ method: 'POST',
+ uri: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
+ json: {
+ signature: null,
+ data: null
}
+ }
- const host = CONFIG.WEBSERVER.HOST
+ const host = CONFIG.WEBSERVER.HOST
- let dataToSign
- if (params.data) {
- dataToSign = params.data
- } else {
- // We do not have data to sign so we just take our host
- // It is not ideal but the connection should be in HTTPS
- dataToSign = host
- }
+ let dataToSign
+ if (params.data) {
+ dataToSign = params.data
+ } else {
+ // We do not have data to sign so we just take our host
+ // It is not ideal but the connection should be in HTTPS
+ dataToSign = host
+ }
- sign(dataToSign).then(signature => {
- requestParams.json.signature = {
- host, // Which host we pretend to be
- signature
- }
+ sign(dataToSign).then(signature => {
+ requestParams.json.signature = {
+ host, // Which host we pretend to be
+ signature
+ }
- // If there are data information
- if (params.data) {
- requestParams.json.data = params.data
- }
+ // If there are data information
+ if (params.data) {
+ requestParams.json.data = params.data
+ }
- request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body }))
- })
+ return doRequest(requestParams)
})
}
// ---------------------------------------------------------------------------
export {
+ doRequest,
makeRetryRequest,
makeSecureRequest
}
--- /dev/null
+import * as WebFinger from 'webfinger.js'
+
+import { isTestInstance } from './core-utils'
+import { isActivityPubUrlValid } from './custom-validators'
+import { WebFingerData } from '../../shared'
+import { fetchRemoteAccountAndCreatePod } from './activitypub'
+
+const webfinger = new WebFinger({
+ webfist_fallback: false,
+ tls_only: isTestInstance(),
+ uri_fallback: false,
+ request_timeout: 3000
+})
+
+async function getAccountFromWebfinger (url: string) {
+ const webfingerData: WebFingerData = await webfingerLookup(url)
+
+ if (Array.isArray(webfingerData.links) === false) return undefined
+
+ const selfLink = webfingerData.links.find(l => l.rel === 'self')
+ if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined
+
+ const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href)
+
+ return account
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ getAccountFromWebfinger
+}
+
+// ---------------------------------------------------------------------------
+
+function webfingerLookup (url: string) {
+ return new Promise<WebFingerData>((res, rej) => {
+ webfinger.lookup('nick@silverbucket.net', (err, p) => {
+ if (err) return rej(err)
+
+ return p
+ })
+ })
+}
import { promisify0 } from '../helpers/core-utils'
import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
-import { UserModel } from '../models/user/user-interface'
+import { UserModel } from '../models/account/user-interface'
// Some checks on configuration files
function checkConfig () {
RequestVideoEventType,
RequestVideoQaduType,
RemoteVideoRequestType,
- JobState
+ JobState,
+ JobCategory
} from '../../shared/models'
import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
PASSWORD: config.get<string>('database.password')
},
STORAGE: {
- CERT_DIR: join(root(), config.get<string>('storage.certs')),
LOG_DIR: join(root(), config.get<string>('storage.logs')),
VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')),
MAX: 1000
}
+const ACTIVITY_PUB = {
+ COLLECTION_ITEMS_PER_PAGE: 10
+}
+
// ---------------------------------------------------------------------------
// Number of points we add/remove from a friend after a successful/bad request
ERROR: 'error',
SUCCESS: 'success'
}
+const JOB_CATEGORIES: { [ id: string ]: JobCategory } = {
+ TRANSCODING: 'transcoding',
+ HTTP_REQUEST: 'http-request'
+}
// How many maximum jobs we fetch from the database per cycle
-const JOBS_FETCH_LIMIT_PER_CYCLE = 10
+const JOBS_FETCH_LIMIT_PER_CYCLE = {
+ transcoding: 10,
+ httpRequest: 20
+}
// 1 minutes
let JOBS_FETCHING_INTERVAL = 60000
// ---------------------------------------------------------------------------
-const PRIVATE_CERT_NAME = 'peertube.key.pem'
-const PUBLIC_CERT_NAME = 'peertube.pub'
-const SIGNATURE_ALGORITHM = 'RSA-SHA256'
-const SIGNATURE_ENCODING = 'hex'
+// const SIGNATURE_ALGORITHM = 'RSA-SHA256'
+// const SIGNATURE_ENCODING = 'hex'
+const PRIVATE_RSA_KEY_SIZE = 2048
// Password encryption
const BCRYPT_SALT_SIZE = 10
JOB_STATES,
JOBS_FETCH_LIMIT_PER_CYCLE,
JOBS_FETCHING_INTERVAL,
+ JOB_CATEGORIES,
LAST_MIGRATION_VERSION,
OAUTH_LIFETIME,
OPENGRAPH_AND_OEMBED_COMMENT,
PAGINATION_COUNT_DEFAULT,
PODS_SCORE,
PREVIEWS_SIZE,
- PRIVATE_CERT_NAME,
- PUBLIC_CERT_NAME,
REMOTE_SCHEME,
REQUEST_ENDPOINT_ACTIONS,
REQUEST_ENDPOINTS,
REQUESTS_VIDEO_QADU_LIMIT_PODS,
RETRY_REQUESTS,
SEARCHABLE_COLUMNS,
- SIGNATURE_ALGORITHM,
- SIGNATURE_ENCODING,
+ PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,
STATIC_MAX_AGE,
STATIC_PATHS,
+ ACTIVITY_PUB,
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
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 { UserModel } from '../models/account/user-interface'
+import { AccountVideoRateModel } from '../models/account/account-video-rate-interface'
+import { AccountFollowModel } from '../models/account/account-follow-interface'
import { TagModel } from './../models/video/tag-interface'
import { RequestModel } from './../models/request/request-interface'
import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface'
import { OAuthTokenModel } from './../models/oauth/oauth-token-interface'
import { OAuthClientModel } from './../models/oauth/oauth-client-interface'
import { JobModel } from './../models/job/job-interface'
-import { AuthorModel } from './../models/video/author-interface'
+import { AccountModel } from './../models/account/account-interface'
import { ApplicationModel } from './../models/application/application-interface'
const dbname = CONFIG.DATABASE.DBNAME
init?: (silent: boolean) => Promise<void>,
Application?: ApplicationModel,
- Author?: AuthorModel,
+ Account?: AccountModel,
Job?: JobModel,
OAuthClient?: OAuthClientModel,
OAuthToken?: OAuthTokenModel,
RequestVideoQadu?: RequestVideoQaduModel,
Request?: RequestModel,
Tag?: TagModel,
- UserVideoRate?: UserVideoRateModel,
+ AccountVideoRate?: AccountVideoRateModel,
+ AccountFollow?: AccountFollowModel,
User?: UserModel,
VideoAbuse?: VideoAbuseModel,
VideoChannel?: VideoChannelModel,
return true
})
- const tasks: Bluebird<any>[] = []
+ const tasks: Promise<any>[] = []
// For each directory we read it and append model in the modelFilePaths array
for (const directory of directories) {
--- /dev/null
+export * from './process-create'
+export * from './process-flag'
+export * from './process-update'
--- /dev/null
+import {
+ ActivityCreate,
+ VideoTorrentObject,
+ VideoChannelObject
+} from '../../../shared'
+import { database as db } from '../../initializers'
+import { logger, retryTransactionWrapper } from '../../helpers'
+
+function processCreateActivity (activity: ActivityCreate) {
+ const activityObject = activity.object
+ const activityType = activityObject.type
+
+ if (activityType === 'Video') {
+ return processCreateVideo(activityObject as VideoTorrentObject)
+ } else if (activityType === 'VideoChannel') {
+ return processCreateVideoChannel(activityObject as VideoChannelObject)
+ }
+
+ logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
+ return Promise.resolve()
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ processCreateActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processCreateVideo (video: VideoTorrentObject) {
+ const options = {
+ arguments: [ video ],
+ errorMessage: 'Cannot insert the remote video with many retries.'
+ }
+
+ return retryTransactionWrapper(addRemoteVideo, options)
+}
+
+async function addRemoteVideo (videoToCreateData: VideoTorrentObject) {
+ logger.debug('Adding remote video %s.', videoToCreateData.url)
+
+ 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)
+}
+
+function processCreateVideoChannel (videoChannel: VideoChannelObject) {
+
+}
--- /dev/null
+import {
+ ActivityCreate,
+ VideoTorrentObject,
+ VideoChannelObject
+} from '../../../shared'
+
+function processFlagActivity (activity: ActivityCreate) {
+ // empty
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ processFlagActivity
+}
+
+// ---------------------------------------------------------------------------
--- /dev/null
+import {
+ ActivityCreate,
+ VideoTorrentObject,
+ VideoChannelObject
+} from '../../../shared'
+
+function processUpdateActivity (activity: ActivityCreate) {
+ if (activity.object.type === 'Video') {
+ return processUpdateVideo(activity.object)
+ } else if (activity.object.type === 'VideoChannel') {
+ return processUpdateVideoChannel(activity.object)
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ processUpdateActivity
+}
+
+// ---------------------------------------------------------------------------
+
+function processUpdateVideo (video: VideoTorrentObject) {
+
+}
+
+function processUpdateVideoChannel (videoChannel: VideoChannelObject) {
+
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+import {
+ AccountInstance,
+ VideoInstance,
+ VideoChannelInstance
+} from '../../models'
+import { httpRequestJobScheduler } from '../jobs'
+import { signObject, activityPubContextify } from '../../helpers'
+import { Activity } from '../../../shared'
+
+function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+ const videoChannelObject = videoChannel.toActivityPubObject()
+ const data = createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+ const videoChannelObject = videoChannel.toActivityPubObject()
+ const data = updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+ const videoChannelObject = videoChannel.toActivityPubObject()
+ const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
+ const videoObject = video.toActivityPubObject()
+ const data = addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
+ const videoObject = video.toActivityPubObject()
+ const data = updateActivityData(video.url, video.VideoChannel.Account, videoObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
+ const videoObject = video.toActivityPubObject()
+ const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject)
+
+ return broadcastToFollowers(data, t)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+
+}
+
+// ---------------------------------------------------------------------------
+
+function broadcastToFollowers (data: any, t: Sequelize.Transaction) {
+ return httpRequestJobScheduler.createJob(t, 'http-request', 'httpRequestBroadcastHandler', data)
+}
+
+function buildSignedActivity (byAccount: AccountInstance, data: Object) {
+ const activity = activityPubContextify(data)
+
+ return signObject(byAccount, activity) as Promise<Activity>
+}
+
+async function getPublicActivityTo (account: AccountInstance) {
+ const inboxUrls = await account.getFollowerSharedInboxUrls()
+
+ return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public')
+}
+
+async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
+ const to = await getPublicActivityTo(byAccount)
+ const base = {
+ type: 'Create',
+ id: url,
+ actor: byAccount.url,
+ to,
+ object
+ }
+
+ return buildSignedActivity(byAccount, base)
+}
+
+async function updateActivityData (url: string, byAccount: AccountInstance, object: any) {
+ const to = await getPublicActivityTo(byAccount)
+ const base = {
+ type: 'Update',
+ id: url,
+ actor: byAccount.url,
+ to,
+ object
+ }
+
+ return buildSignedActivity(byAccount, base)
+}
+
+async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) {
+ const to = await getPublicActivityTo(byAccount)
+ const base = {
+ type: 'Update',
+ id: url,
+ actor: byAccount.url,
+ to,
+ object
+ }
+
+ return buildSignedActivity(byAccount, base)
+}
+
+async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) {
+ const to = await getPublicActivityTo(byAccount)
+ const base = {
+ type: 'Add',
+ id: url,
+ actor: byAccount.url,
+ to,
+ object,
+ target
+ }
+
+ return buildSignedActivity(byAccount, base)
+}
+export * from './activitypub'
export * from './cache'
export * from './jobs'
export * from './request'
+++ /dev/null
-import * as videoFileOptimizer from './video-file-optimizer'
-import * as videoFileTranscoder from './video-file-transcoder'
-
-export interface JobHandler<T> {
- process (data: object, jobId: number): T
- onError (err: Error, jobId: number)
- onSuccess (jobId: number, jobResult: T)
-}
-
-const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
- videoFileOptimizer,
- videoFileTranscoder
-}
-
-export {
- jobHandlers
-}
+++ /dev/null
-import * as Bluebird from 'bluebird'
-
-import { database as db } from '../../../initializers/database'
-import { logger, computeResolutionsToTranscode } from '../../../helpers'
-import { VideoInstance } from '../../../models'
-import { addVideoToFriends } from '../../friends'
-import { JobScheduler } from '../job-scheduler'
-
-async function process (data: { videoUUID: string }, jobId: number) {
- const video = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID)
- // No video, maybe deleted?
- if (!video) {
- logger.info('Do not process job %d, video does not exist.', jobId, { videoUUID: video.uuid })
- return undefined
- }
-
- await video.optimizeOriginalVideofile()
-
- return video
-}
-
-function onError (err: Error, jobId: number) {
- logger.error('Error when optimized video file in job %d.', jobId, err)
- return Promise.resolve()
-}
-
-async function onSuccess (jobId: number, video: VideoInstance) {
- if (video === undefined) return undefined
-
- logger.info('Job %d is a success.', jobId)
-
- // Maybe the video changed in database, refresh it
- const videoDatabase = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(video.uuid)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
-
- const remoteVideo = await videoDatabase.toAddRemoteJSON()
-
- // Now we'll add the video's meta data to our friends
- await addVideoToFriends(remoteVideo, null)
-
- const originalFileHeight = await videoDatabase.getOriginalFileHeight()
- // Create transcoding jobs if there are enabled resolutions
-
- const resolutionsEnabled = computeResolutionsToTranscode(originalFileHeight)
- logger.info(
- 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, originalFileHeight,
- { resolutions: resolutionsEnabled }
- )
-
- if (resolutionsEnabled.length !== 0) {
- try {
- await db.sequelize.transaction(async t => {
- const tasks: Bluebird<any>[] = []
-
- for (const resolution of resolutionsEnabled) {
- const dataInput = {
- videoUUID: videoDatabase.uuid,
- resolution
- }
-
- const p = JobScheduler.Instance.createJob(t, 'videoFileTranscoder', dataInput)
- tasks.push(p)
- }
-
- await Promise.all(tasks)
- })
-
- logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
- } catch (err) {
- logger.warn('Cannot transcode the video.', err)
- }
- } else {
- logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
- return undefined
- }
-}
-
-// ---------------------------------------------------------------------------
-
-export {
- process,
- onError,
- onSuccess
-}
+++ /dev/null
-import { database as db } from '../../../initializers/database'
-import { updateVideoToFriends } from '../../friends'
-import { logger } from '../../../helpers'
-import { VideoInstance } from '../../../models'
-import { VideoResolution } from '../../../../shared'
-
-async function process (data: { videoUUID: string, resolution: VideoResolution }, jobId: number) {
- const video = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID)
- // No video, maybe deleted?
- if (!video) {
- logger.info('Do not process job %d, video does not exist.', jobId, { videoUUID: video.uuid })
- return undefined
- }
-
- await video.transcodeOriginalVideofile(data.resolution)
-
- return video
-}
-
-function onError (err: Error, jobId: number) {
- logger.error('Error when transcoding video file in job %d.', jobId, err)
- return Promise.resolve()
-}
-
-async function onSuccess (jobId: number, video: VideoInstance) {
- if (video === undefined) return undefined
-
- logger.info('Job %d is a success.', jobId)
-
- // Maybe the video changed in database, refresh it
- const videoDatabase = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(video.uuid)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
-
- const remoteVideo = videoDatabase.toUpdateRemoteJSON()
-
- // Now we'll add the video's meta data to our friends
- await updateVideoToFriends(remoteVideo, null)
-
- return undefined
-}
-
-// ---------------------------------------------------------------------------
-
-export {
- process,
- onError,
- onSuccess
-}
--- /dev/null
+import * as Bluebird from 'bluebird'
+
+import { database as db } from '../../../initializers/database'
+import { logger } from '../../../helpers'
+
+async function process (data: { videoUUID: string }, jobId: number) {
+
+}
+
+function onError (err: Error, jobId: number) {
+ logger.error('Error when optimized video file in job %d.', jobId, err)
+ return Promise.resolve()
+}
+
+async function onSuccess (jobId: number) {
+
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ process,
+ onError,
+ onSuccess
+}
--- /dev/null
+import { JobScheduler, JobHandler } from '../job-scheduler'
+
+import * as httpRequestBroadcastHandler from './http-request-broadcast-handler'
+import * as httpRequestUnicastHandler from './http-request-unicast-handler'
+import { JobCategory } from '../../../../shared'
+
+const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
+ httpRequestBroadcastHandler,
+ httpRequestUnicastHandler
+}
+const jobCategory: JobCategory = 'http-request'
+
+const httpRequestJobScheduler = new JobScheduler(jobCategory, jobHandlers)
+
+export {
+ httpRequestJobScheduler
+}
--- /dev/null
+import * as Bluebird from 'bluebird'
+
+import { database as db } from '../../../initializers/database'
+import { logger } from '../../../helpers'
+
+async function process (data: { videoUUID: string }, jobId: number) {
+
+}
+
+function onError (err: Error, jobId: number) {
+ logger.error('Error when optimized video file in job %d.', jobId, err)
+ return Promise.resolve()
+}
+
+async function onSuccess (jobId: number) {
+
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ process,
+ onError,
+ onSuccess
+}
--- /dev/null
+export * from './http-request-job-scheduler'
-export * from './job-scheduler'
+export * from './http-request-job-scheduler'
+export * from './transcoding-job-scheduler'
import { AsyncQueue, forever, queue } from 'async'
import * as Sequelize from 'sequelize'
-import { database as db } from '../../initializers/database'
import {
+ database as db,
JOBS_FETCHING_INTERVAL,
JOBS_FETCH_LIMIT_PER_CYCLE,
JOB_STATES
} from '../../initializers'
import { logger } from '../../helpers'
import { JobInstance } from '../../models'
-import { JobHandler, jobHandlers } from './handlers'
+import { JobCategory } from '../../../shared'
+export interface JobHandler<T> {
+ process (data: object, jobId: number): T
+ onError (err: Error, jobId: number)
+ onSuccess (jobId: number, jobResult: T)
+}
type JobQueueCallback = (err: Error) => void
-class JobScheduler {
-
- private static instance: JobScheduler
+class JobScheduler<T> {
- private constructor () { }
-
- static get Instance () {
- return this.instance || (this.instance = new this())
- }
+ constructor (
+ private jobCategory: JobCategory,
+ private jobHandlers: { [ id: string ]: JobHandler<T> }
+ ) {}
async activate () {
- const limit = JOBS_FETCH_LIMIT_PER_CYCLE
+ const limit = JOBS_FETCH_LIMIT_PER_CYCLE[this.jobCategory]
- logger.info('Jobs scheduler activated.')
+ logger.info('Jobs scheduler %s activated.', this.jobCategory)
const jobsQueue = queue<JobInstance, JobQueueCallback>(this.processJob.bind(this))
// Finish processing jobs from a previous start
const state = JOB_STATES.PROCESSING
try {
- const jobs = await db.Job.listWithLimit(limit, state)
+ const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
this.enqueueJobs(jobsQueue, jobs)
} catch (err) {
const state = JOB_STATES.PENDING
try {
- const jobs = await db.Job.listWithLimit(limit, state)
+ const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
this.enqueueJobs(jobsQueue, jobs)
} catch (err) {
)
}
- createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object) {
+ createJob (transaction: Sequelize.Transaction, category: JobCategory, handlerName: string, handlerInputData: object) {
const createQuery = {
state: JOB_STATES.PENDING,
+ category,
handlerName,
handlerInputData
}
}
private async processJob (job: JobInstance, callback: (err: Error) => void) {
- const jobHandler = jobHandlers[job.handlerName]
+ const jobHandler = this.jobHandlers[job.handlerName]
if (jobHandler === undefined) {
logger.error('Unknown job handler for job %s.', job.handlerName)
return callback(null)
--- /dev/null
+export * from './transcoding-job-scheduler'
--- /dev/null
+import { JobScheduler, JobHandler } from '../job-scheduler'
+
+import * as videoFileOptimizer from './video-file-optimizer-handler'
+import * as videoFileTranscoder from './video-file-transcoder-handler'
+import { JobCategory } from '../../../../shared'
+
+const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
+ videoFileOptimizer,
+ videoFileTranscoder
+}
+const jobCategory: JobCategory = 'transcoding'
+
+const transcodingJobScheduler = new JobScheduler(jobCategory, jobHandlers)
+
+export {
+ transcodingJobScheduler
+}
--- /dev/null
+import * as Bluebird from 'bluebird'
+
+import { database as db } from '../../../initializers/database'
+import { logger, computeResolutionsToTranscode } from '../../../helpers'
+import { VideoInstance } from '../../../models'
+import { addVideoToFriends } from '../../friends'
+import { JobScheduler } from '../job-scheduler'
+
+async function process (data: { videoUUID: string }, jobId: number) {
+ const video = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID)
+ // No video, maybe deleted?
+ if (!video) {
+ logger.info('Do not process job %d, video does not exist.', jobId, { videoUUID: video.uuid })
+ return undefined
+ }
+
+ await video.optimizeOriginalVideofile()
+
+ return video
+}
+
+function onError (err: Error, jobId: number) {
+ logger.error('Error when optimized video file in job %d.', jobId, err)
+ return Promise.resolve()
+}
+
+async function onSuccess (jobId: number, video: VideoInstance) {
+ if (video === undefined) return undefined
+
+ logger.info('Job %d is a success.', jobId)
+
+ // Maybe the video changed in database, refresh it
+ const videoDatabase = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(video.uuid)
+ // Video does not exist anymore
+ if (!videoDatabase) return undefined
+
+ const remoteVideo = await videoDatabase.toAddRemoteJSON()
+
+ // Now we'll add the video's meta data to our friends
+ await addVideoToFriends(remoteVideo, null)
+
+ const originalFileHeight = await videoDatabase.getOriginalFileHeight()
+ // Create transcoding jobs if there are enabled resolutions
+
+ const resolutionsEnabled = computeResolutionsToTranscode(originalFileHeight)
+ logger.info(
+ 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, originalFileHeight,
+ { resolutions: resolutionsEnabled }
+ )
+
+ if (resolutionsEnabled.length !== 0) {
+ try {
+ await db.sequelize.transaction(async t => {
+ const tasks: Bluebird<any>[] = []
+
+ for (const resolution of resolutionsEnabled) {
+ const dataInput = {
+ videoUUID: videoDatabase.uuid,
+ resolution
+ }
+
+ const p = JobScheduler.Instance.createJob(t, 'videoFileTranscoder', dataInput)
+ tasks.push(p)
+ }
+
+ await Promise.all(tasks)
+ })
+
+ logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
+ } catch (err) {
+ logger.warn('Cannot transcode the video.', err)
+ }
+ } else {
+ logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
+ return undefined
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ process,
+ onError,
+ onSuccess
+}
--- /dev/null
+import { database as db } from '../../../initializers/database'
+import { updateVideoToFriends } from '../../friends'
+import { logger } from '../../../helpers'
+import { VideoInstance } from '../../../models'
+import { VideoResolution } from '../../../../shared'
+
+async function process (data: { videoUUID: string, resolution: VideoResolution }, jobId: number) {
+ const video = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(data.videoUUID)
+ // No video, maybe deleted?
+ if (!video) {
+ logger.info('Do not process job %d, video does not exist.', jobId, { videoUUID: video.uuid })
+ return undefined
+ }
+
+ await video.transcodeOriginalVideofile(data.resolution)
+
+ return video
+}
+
+function onError (err: Error, jobId: number) {
+ logger.error('Error when transcoding video file in job %d.', jobId, err)
+ return Promise.resolve()
+}
+
+async function onSuccess (jobId: number, video: VideoInstance) {
+ if (video === undefined) return undefined
+
+ logger.info('Job %d is a success.', jobId)
+
+ // Maybe the video changed in database, refresh it
+ const videoDatabase = await db.Video.loadByUUIDAndPopulateAuthorAndPodAndTags(video.uuid)
+ // Video does not exist anymore
+ if (!videoDatabase) return undefined
+
+ const remoteVideo = videoDatabase.toUpdateRemoteJSON()
+
+ // Now we'll add the video's meta data to our friends
+ await updateVideoToFriends(remoteVideo, null)
+
+ return undefined
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ process,
+ onError,
+ onSuccess
+}
import { database as db } from '../initializers'
import { UserInstance } from '../models'
-import { addVideoAuthorToFriends } from './friends'
+import { addVideoAccountToFriends } from './friends'
import { createVideoChannel } from './video-channel'
-async function createUserAuthorAndChannel (user: UserInstance, validateUser = true) {
+async function createUserAccountAndChannel (user: UserInstance, validateUser = true) {
const res = await db.sequelize.transaction(async t => {
const userOptions = {
transaction: t,
}
const userCreated = await user.save(userOptions)
- const authorInstance = db.Author.build({
+ const accountInstance = db.Account.build({
name: userCreated.username,
podId: null, // It is our pod
userId: userCreated.id
})
- const authorCreated = await authorInstance.save({ transaction: t })
+ const accountCreated = await accountInstance.save({ transaction: t })
- const remoteVideoAuthor = authorCreated.toAddRemoteJSON()
+ const remoteVideoAccount = accountCreated.toAddRemoteJSON()
// Now we'll add the video channel's meta data to our friends
- const author = await addVideoAuthorToFriends(remoteVideoAuthor, t)
+ const account = await addVideoAccountToFriends(remoteVideoAccount, t)
const videoChannelInfo = {
name: `Default ${userCreated.username} channel`
}
- const videoChannel = await createVideoChannel(videoChannelInfo, authorCreated, t)
+ const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
- return { author, videoChannel }
+ return { account, videoChannel }
})
return res
// ---------------------------------------------------------------------------
export {
- createUserAuthorAndChannel
+ createUserAccountAndChannel
}
import { addVideoChannelToFriends } from './friends'
import { database as db } from '../initializers'
import { logger } from '../helpers'
-import { AuthorInstance } from '../models'
+import { AccountInstance } from '../models'
import { VideoChannelCreate } from '../../shared/models'
-async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) {
+async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) {
const videoChannelData = {
name: videoChannelInfo.name,
description: videoChannelInfo.description,
remote: false,
- authorId: author.id
+ authorId: account.id
}
const videoChannel = db.VideoChannel.build(videoChannelData)
const videoChannelCreated = await videoChannel.save(options)
- // Do not forget to add Author information to the created video channel
- videoChannelCreated.Author = author
+ // Do not forget to add Account information to the created video channel
+ videoChannelCreated.Account = account
const remoteVideoChannel = videoChannelCreated.toAddRemoteJSON()
--- /dev/null
+import { Request, Response, NextFunction } from 'express'
+
+import { database as db } from '../initializers'
+import {
+ logger,
+ getAccountFromWebfinger,
+ isSignatureVerified
+} from '../helpers'
+import { ActivityPubSignature } from '../../shared'
+
+async function checkSignature (req: Request, res: Response, next: NextFunction) {
+ const signatureObject: ActivityPubSignature = req.body.signature
+
+ logger.debug('Checking signature of account %s...', signatureObject.creator)
+
+ let account = await db.Account.loadByUrl(signatureObject.creator)
+
+ // We don't have this account in our database, fetch it on remote
+ if (!account) {
+ account = await getAccountFromWebfinger(signatureObject.creator)
+
+ if (!account) {
+ return res.sendStatus(403)
+ }
+
+ // Save our new account in database
+ await account.save()
+ }
+
+ const verified = await isSignatureVerified(account, req.body)
+ if (verified === false) return res.sendStatus(403)
+
+ res.locals.signature.account = account
+
+ return next()
+}
+
+function executeIfActivityPub (fun: any | any[]) {
+ return (req: Request, res: Response, next: NextFunction) => {
+ if (req.header('Accept') !== 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') {
+ return next()
+ }
+
+ if (Array.isArray(fun) === true) {
+ fun[0](req, res, next) // FIXME: doesn't work
+ }
+
+ return fun(req, res, next)
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ checkSignature,
+ executeIfActivityPub
+}
export * from './validators'
+export * from './activitypub'
export * from './async'
export * from './oauth'
export * from './pagination'
export * from './pods'
export * from './search'
-export * from './secure'
export * from './sort'
export * from './user-right'
+++ /dev/null
-import 'express-validator'
-import * as express from 'express'
-
-import { database as db } from '../initializers'
-import {
- logger,
- checkSignature as peertubeCryptoCheckSignature
-} from '../helpers'
-import { PodSignature } from '../../shared'
-
-async function checkSignature (req: express.Request, res: express.Response, next: express.NextFunction) {
- const signatureObject: PodSignature = req.body.signature
- const host = signatureObject.host
-
- try {
- const pod = await db.Pod.loadByHost(host)
- if (pod === null) {
- logger.error('Unknown pod %s.', host)
- return res.sendStatus(403)
- }
-
- logger.debug('Checking signature from %s.', host)
-
- let signatureShouldBe
- // If there is data in the body the sender used it for its signature
- // If there is no data we just use its host as signature
- if (req.body.data) {
- signatureShouldBe = req.body.data
- } else {
- signatureShouldBe = host
- }
-
- const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, signatureObject.signature)
-
- if (signatureOk === true) {
- res.locals.secure = {
- pod
- }
-
- return next()
- }
-
- logger.error('Signature is not okay in body for %s.', signatureObject.host)
- return res.sendStatus(403)
- } catch (err) {
- logger.error('Cannot get signed host in body.', { error: err.stack, signature: signatureObject.signature })
- return res.sendStatus(500)
- }
-}
-
-// ---------------------------------------------------------------------------
-
-export {
- checkSignature
-}
--- /dev/null
+import { param } from 'express-validator/check'
+import * as express from 'express'
+
+import { database as db } from '../../initializers/database'
+import { checkErrors } from './utils'
+import {
+ logger,
+ isUserUsernameValid,
+ isUserPasswordValid,
+ isUserVideoQuotaValid,
+ isUserDisplayNSFWValid,
+ isUserRoleValid,
+ isAccountNameValid
+} from '../../helpers'
+import { AccountInstance } from '../../models'
+
+const localAccountValidator = [
+ param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
+
+ checkErrors(req, res, () => {
+ checkLocalAccountExists(req.params.name, res, next)
+ })
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ localAccountValidator
+}
+
+// ---------------------------------------------------------------------------
+
+function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
+ db.Account.loadLocalAccountByName(name)
+ .then(account => {
+ if (!account) {
+ return res.status(404)
+ .send({ error: 'Account not found' })
+ .end()
+ }
+
+ res.locals.account = account
+ return callback(null, account)
+ })
+ .catch(err => {
+ logger.error('Error in account request validator.', err)
+ return res.sendStatus(500)
+ })
+}
--- /dev/null
+export * from './pods'
+export * from './signature'
+export * from './videos'
--- /dev/null
+import { body } from 'express-validator/check'
+import * as express from 'express'
+
+import { database as db } from '../../../initializers'
+import { isHostValid, logger } from '../../../helpers'
+import { checkErrors } from '../utils'
+
+const remotePodsAddValidator = [
+ body('host').custom(isHostValid).withMessage('Should have a host'),
+ body('email').isEmail().withMessage('Should have an email'),
+ body('publicKey').not().isEmpty().withMessage('Should have a public key'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking podsAdd parameters', { parameters: req.body })
+
+ checkErrors(req, res, () => {
+ db.Pod.loadByHost(req.body.host)
+ .then(pod => {
+ // Pod with this host already exists
+ if (pod) {
+ return res.sendStatus(409)
+ }
+
+ return next()
+ })
+ .catch(err => {
+ logger.error('Cannot load pod by host.', err)
+ res.sendStatus(500)
+ })
+ })
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ remotePodsAddValidator
+}
--- /dev/null
+import { body } from 'express-validator/check'
+import * as express from 'express'
+
+import {
+ logger,
+ isDateValid,
+ isSignatureTypeValid,
+ isSignatureCreatorValid,
+ isSignatureValueValid
+} from '../../../helpers'
+import { checkErrors } from '../utils'
+
+const signatureValidator = [
+ body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
+ body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'),
+ body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
+ body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } })
+
+ checkErrors(req, res, next)
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ signatureValidator
+}
--- /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 * from './account'
export * from './oembed'
-export * from './remote'
+export * from './activitypub'
export * from './pagination'
export * from './pods'
export * from './sort'
+++ /dev/null
-export * from './pods'
-export * from './signature'
-export * from './videos'
+++ /dev/null
-import { body } from 'express-validator/check'
-import * as express from 'express'
-
-import { database as db } from '../../../initializers'
-import { isHostValid, logger } from '../../../helpers'
-import { checkErrors } from '../utils'
-
-const remotePodsAddValidator = [
- body('host').custom(isHostValid).withMessage('Should have a host'),
- body('email').isEmail().withMessage('Should have an email'),
- body('publicKey').not().isEmpty().withMessage('Should have a public key'),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking podsAdd parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- db.Pod.loadByHost(req.body.host)
- .then(pod => {
- // Pod with this host already exists
- if (pod) {
- return res.sendStatus(409)
- }
-
- return next()
- })
- .catch(err => {
- logger.error('Cannot load pod by host.', err)
- res.sendStatus(500)
- })
- })
- }
-]
-
-// ---------------------------------------------------------------------------
-
-export {
- remotePodsAddValidator
-}
+++ /dev/null
-import { body } from 'express-validator/check'
-import * as express from 'express'
-
-import { logger, isHostValid } from '../../../helpers'
-import { checkErrors } from '../utils'
-
-const signatureValidator = [
- body('signature.host').custom(isHostValid).withMessage('Should have a signature host'),
- body('signature.signature').not().isEmpty().withMessage('Should have a signature'),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking signature parameters', { parameters: { signature: req.body.signature } })
-
- checkErrors(req, res, next)
- }
-]
-
-// ---------------------------------------------------------------------------
-
-export {
- signatureValidator
-}
+++ /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
-}
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+
+import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
+
+export namespace AccountFollowMethods {
+}
+
+export interface AccountFollowClass {
+}
+
+export interface AccountFollowAttributes {
+ accountId: number
+ targetAccountId: number
+}
+
+export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
+ id: number
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+import { addMethodsToModel } from '../utils'
+import {
+ AccountFollowInstance,
+ AccountFollowAttributes,
+
+ AccountFollowMethods
+} from './account-follow-interface'
+
+let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+ AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
+ { },
+ {
+ indexes: [
+ {
+ fields: [ 'accountId' ],
+ unique: true
+ },
+ {
+ fields: [ 'targetAccountId' ],
+ unique: true
+ }
+ ]
+ }
+ )
+
+ const classMethods = [
+ associate
+ ]
+ addMethodsToModel(AccountFollow, classMethods)
+
+ return AccountFollow
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ AccountFollow.belongsTo(models.Account, {
+ foreignKey: {
+ name: 'accountId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+
+ AccountFollow.belongsTo(models.Account, {
+ foreignKey: {
+ name: 'targetAccountId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Bluebird from 'bluebird'
+
+import { PodInstance } from '../pod/pod-interface'
+import { VideoChannelInstance } from '../video/video-channel-interface'
+import { ActivityPubActor } from '../../../shared'
+import { ResultList } from '../../../shared/models/result-list.model'
+
+export namespace AccountMethods {
+ export type Load = (id: number) => Bluebird<AccountInstance>
+ export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
+ export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
+ export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
+ export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
+ export type ListOwned = () => Bluebird<AccountInstance[]>
+ export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
+ export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
+
+ export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
+ export type IsOwned = (this: AccountInstance) => boolean
+ export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
+ export type GetFollowingUrl = (this: AccountInstance) => string
+ export type GetFollowersUrl = (this: AccountInstance) => string
+ export type GetPublicKeyUrl = (this: AccountInstance) => string
+}
+
+export interface AccountClass {
+ loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
+ load: AccountMethods.Load
+ loadByUUID: AccountMethods.LoadByUUID
+ loadByUrl: AccountMethods.LoadByUrl
+ loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
+ listOwned: AccountMethods.ListOwned
+ listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
+ listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
+}
+
+export interface AccountAttributes {
+ name: string
+ url: string
+ publicKey: string
+ privateKey: string
+ followersCount: number
+ followingCount: number
+ inboxUrl: string
+ outboxUrl: string
+ sharedInboxUrl: string
+ followersUrl: string
+ followingUrl: string
+
+ uuid?: string
+
+ podId?: number
+ userId?: number
+ applicationId?: number
+}
+
+export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
+ isOwned: AccountMethods.IsOwned
+ toActivityPubObject: AccountMethods.ToActivityPubObject
+ getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
+ getFollowingUrl: AccountMethods.GetFollowingUrl
+ getFollowersUrl: AccountMethods.GetFollowersUrl
+ getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
+
+ id: number
+ createdAt: Date
+ updatedAt: Date
+
+ Pod: PodInstance
+ VideoChannels: VideoChannelInstance[]
+}
+
+export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+
+import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
+
+export namespace AccountVideoRateMethods {
+ export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
+}
+
+export interface AccountVideoRateClass {
+ load: AccountVideoRateMethods.Load
+}
+
+export interface AccountVideoRateAttributes {
+ type: VideoRateType
+ accountId: number
+ videoId: number
+}
+
+export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
+ id: number
+ createdAt: Date
+ updatedAt: Date
+}
+
+export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}
--- /dev/null
+/*
+ Account rates per video.
+*/
+import { values } from 'lodash'
+import * as Sequelize from 'sequelize'
+
+import { VIDEO_RATE_TYPES } from '../../initializers'
+
+import { addMethodsToModel } from '../utils'
+import {
+ AccountVideoRateInstance,
+ AccountVideoRateAttributes,
+
+ AccountVideoRateMethods
+} from './account-video-rate-interface'
+
+let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
+let load: AccountVideoRateMethods.Load
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+ AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
+ {
+ type: {
+ type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
+ allowNull: false
+ }
+ },
+ {
+ indexes: [
+ {
+ fields: [ 'videoId', 'accountId', 'type' ],
+ unique: true
+ }
+ ]
+ }
+ )
+
+ const classMethods = [
+ associate,
+
+ load
+ ]
+ addMethodsToModel(AccountVideoRate, classMethods)
+
+ return AccountVideoRate
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ AccountVideoRate.belongsTo(models.Video, {
+ foreignKey: {
+ name: 'videoId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+
+ AccountVideoRate.belongsTo(models.Account, {
+ foreignKey: {
+ name: 'accountId',
+ allowNull: false
+ },
+ onDelete: 'CASCADE'
+ })
+}
+
+load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) {
+ const options: Sequelize.FindOptions<AccountVideoRateAttributes> = {
+ where: {
+ accountId,
+ videoId
+ }
+ }
+ if (transaction) options.transaction = transaction
+
+ return AccountVideoRate.findOne(options)
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+import {
+ isUserUsernameValid,
+ isAccountPublicKeyValid,
+ isAccountUrlValid,
+ isAccountPrivateKeyValid,
+ isAccountFollowersCountValid,
+ isAccountFollowingCountValid,
+ isAccountInboxValid,
+ isAccountOutboxValid,
+ isAccountSharedInboxValid,
+ isAccountFollowersValid,
+ isAccountFollowingValid,
+ activityPubContextify
+} from '../../helpers'
+
+import { addMethodsToModel } from '../utils'
+import {
+ AccountInstance,
+ AccountAttributes,
+
+ AccountMethods
+} from './account-interface'
+
+let Account: Sequelize.Model<AccountInstance, AccountAttributes>
+let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
+let load: AccountMethods.Load
+let loadByUUID: AccountMethods.LoadByUUID
+let loadByUrl: AccountMethods.LoadByUrl
+let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
+let listOwned: AccountMethods.ListOwned
+let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
+let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
+let isOwned: AccountMethods.IsOwned
+let toActivityPubObject: AccountMethods.ToActivityPubObject
+let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
+let getFollowingUrl: AccountMethods.GetFollowingUrl
+let getFollowersUrl: AccountMethods.GetFollowersUrl
+let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
+
+export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+ Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
+ {
+ uuid: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ allowNull: false,
+ validate: {
+ isUUID: 4
+ }
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ usernameValid: value => {
+ const res = isUserUsernameValid(value)
+ if (res === false) throw new Error('Username is not valid.')
+ }
+ }
+ },
+ url: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ urlValid: value => {
+ const res = isAccountUrlValid(value)
+ if (res === false) throw new Error('URL is not valid.')
+ }
+ }
+ },
+ publicKey: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ publicKeyValid: value => {
+ const res = isAccountPublicKeyValid(value)
+ if (res === false) throw new Error('Public key is not valid.')
+ }
+ }
+ },
+ privateKey: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ privateKeyValid: value => {
+ const res = isAccountPrivateKeyValid(value)
+ if (res === false) throw new Error('Private key is not valid.')
+ }
+ }
+ },
+ followersCount: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ followersCountValid: value => {
+ const res = isAccountFollowersCountValid(value)
+ if (res === false) throw new Error('Followers count is not valid.')
+ }
+ }
+ },
+ followingCount: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ followersCountValid: value => {
+ const res = isAccountFollowingCountValid(value)
+ if (res === false) throw new Error('Following count is not valid.')
+ }
+ }
+ },
+ inboxUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ inboxUrlValid: value => {
+ const res = isAccountInboxValid(value)
+ if (res === false) throw new Error('Inbox URL is not valid.')
+ }
+ }
+ },
+ outboxUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ outboxUrlValid: value => {
+ const res = isAccountOutboxValid(value)
+ if (res === false) throw new Error('Outbox URL is not valid.')
+ }
+ }
+ },
+ sharedInboxUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ sharedInboxUrlValid: value => {
+ const res = isAccountSharedInboxValid(value)
+ if (res === false) throw new Error('Shared inbox URL is not valid.')
+ }
+ }
+ },
+ followersUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ followersUrlValid: value => {
+ const res = isAccountFollowersValid(value)
+ if (res === false) throw new Error('Followers URL is not valid.')
+ }
+ }
+ },
+ followingUrl: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ followingUrlValid: value => {
+ const res = isAccountFollowingValid(value)
+ if (res === false) throw new Error('Following URL is not valid.')
+ }
+ }
+ }
+ },
+ {
+ indexes: [
+ {
+ fields: [ 'name' ]
+ },
+ {
+ fields: [ 'podId' ]
+ },
+ {
+ fields: [ 'userId' ],
+ unique: true
+ },
+ {
+ fields: [ 'applicationId' ],
+ unique: true
+ },
+ {
+ fields: [ 'name', 'podId' ],
+ unique: true
+ }
+ ],
+ hooks: { afterDestroy }
+ }
+ )
+
+ const classMethods = [
+ associate,
+ loadAccountByPodAndUUID,
+ load,
+ loadByUUID,
+ loadLocalAccountByName,
+ listOwned,
+ listFollowerUrlsForApi,
+ listFollowingUrlsForApi
+ ]
+ const instanceMethods = [
+ isOwned,
+ toActivityPubObject,
+ getFollowerSharedInboxUrls,
+ getFollowingUrl,
+ getFollowersUrl,
+ getPublicKeyUrl
+ ]
+ addMethodsToModel(Account, classMethods, instanceMethods)
+
+ return Account
+}
+
+// ---------------------------------------------------------------------------
+
+function associate (models) {
+ Account.belongsTo(models.Pod, {
+ foreignKey: {
+ name: 'podId',
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+
+ Account.belongsTo(models.User, {
+ foreignKey: {
+ name: 'userId',
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+
+ Account.belongsTo(models.Application, {
+ foreignKey: {
+ name: 'userId',
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+
+ Account.hasMany(models.VideoChannel, {
+ foreignKey: {
+ name: 'accountId',
+ allowNull: false
+ },
+ onDelete: 'cascade',
+ hooks: true
+ })
+
+ Account.hasMany(models.AccountFollower, {
+ foreignKey: {
+ name: 'accountId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+
+ Account.hasMany(models.AccountFollower, {
+ foreignKey: {
+ name: 'targetAccountId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+}
+
+function afterDestroy (account: AccountInstance) {
+ if (account.isOwned()) {
+ const removeVideoAccountToFriendsParams = {
+ uuid: account.uuid
+ }
+
+ return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
+ }
+
+ return undefined
+}
+
+toActivityPubObject = function (this: AccountInstance) {
+ const type = this.podId ? 'Application' : 'Person'
+
+ const json = {
+ type,
+ id: this.url,
+ following: this.getFollowingUrl(),
+ followers: this.getFollowersUrl(),
+ inbox: this.inboxUrl,
+ outbox: this.outboxUrl,
+ preferredUsername: this.name,
+ url: this.url,
+ name: this.name,
+ endpoints: {
+ sharedInbox: this.sharedInboxUrl
+ },
+ uuid: this.uuid,
+ publicKey: {
+ id: this.getPublicKeyUrl(),
+ owner: this.url,
+ publicKeyPem: this.publicKey
+ }
+ }
+
+ return activityPubContextify(json)
+}
+
+isOwned = function (this: AccountInstance) {
+ return this.podId === null
+}
+
+getFollowerSharedInboxUrls = function (this: AccountInstance) {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ attributes: [ 'sharedInboxUrl' ],
+ include: [
+ {
+ model: Account['sequelize'].models.AccountFollower,
+ where: {
+ targetAccountId: this.id
+ }
+ }
+ ]
+ }
+
+ return Account.findAll(query)
+ .then(accounts => accounts.map(a => a.sharedInboxUrl))
+}
+
+getFollowingUrl = function (this: AccountInstance) {
+ return this.url + '/followers'
+}
+
+getFollowersUrl = function (this: AccountInstance) {
+ return this.url + '/followers'
+}
+
+getPublicKeyUrl = function (this: AccountInstance) {
+ return this.url + '#main-key'
+}
+
+// ------------------------------ STATICS ------------------------------
+
+listOwned = function () {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ where: {
+ podId: null
+ }
+ }
+
+ return Account.findAll(query)
+}
+
+listFollowerUrlsForApi = function (name: string, start: number, count: number) {
+ return createListFollowForApiQuery('followers', name, start, count)
+}
+
+listFollowingUrlsForApi = function (name: string, start: number, count: number) {
+ return createListFollowForApiQuery('following', name, start, count)
+}
+
+load = function (id: number) {
+ return Account.findById(id)
+}
+
+loadByUUID = function (uuid: string) {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ where: {
+ uuid
+ }
+ }
+
+ return Account.findOne(query)
+}
+
+loadLocalAccountByName = function (name: string) {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ where: {
+ name,
+ userId: {
+ [Sequelize.Op.ne]: null
+ }
+ }
+ }
+
+ return Account.findOne(query)
+}
+
+loadByUrl = function (url: string) {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ where: {
+ url
+ }
+ }
+
+ return Account.findOne(query)
+}
+
+loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
+ const query: Sequelize.FindOptions<AccountAttributes> = {
+ where: {
+ podId,
+ uuid
+ },
+ transaction
+ }
+
+ return Account.find(query)
+}
+
+// ------------------------------ UTILS ------------------------------
+
+async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) {
+ let firstJoin: string
+ let secondJoin: string
+
+ if (type === 'followers') {
+ firstJoin = 'targetAccountId'
+ secondJoin = 'accountId'
+ } else {
+ firstJoin = 'accountId'
+ secondJoin = 'targetAccountId'
+ }
+
+ const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ]
+ const tasks: Promise<any>[] = []
+
+ for (const selection of selections) {
+ const query = 'SELECT ' + selection + ' FROM "Account" ' +
+ 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
+ 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' +
+ 'WHERE "Account"."name" = \'$name\' ' +
+ 'LIMIT ' + start + ', ' + count
+
+ const options = {
+ bind: { name },
+ type: Sequelize.QueryTypes.SELECT
+ }
+ tasks.push(Account['sequelize'].query(query, options))
+ }
+
+ const [ followers, [ { total } ]] = await Promise.all(tasks)
+ const urls: string[] = followers.map(f => f.url)
+
+ return {
+ data: urls,
+ total: parseInt(total, 10)
+ }
+}
--- /dev/null
+export * from './account-interface'
+export * from './account-follow-interface'
+export * from './account-video-rate-interface'
+export * from './user-interface'
--- /dev/null
+import * as Sequelize from 'sequelize'
+import * as Bluebird from 'bluebird'
+
+// Don't use barrel, import just what we need
+import { AccountInstance } from './account-interface'
+import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import { ResultList } from '../../../shared/models/result-list.model'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
+import { UserRole } from '../../../shared/models/users/user-role'
+
+export namespace UserMethods {
+ export type HasRight = (this: UserInstance, right: UserRight) => boolean
+ export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
+
+ export type ToFormattedJSON = (this: UserInstance) => FormattedUser
+ export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
+
+ export type CountTotal = () => Bluebird<number>
+
+ export type GetByUsername = (username: string) => Bluebird<UserInstance>
+
+ export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
+
+ export type LoadById = (id: number) => Bluebird<UserInstance>
+
+ export type LoadByUsername = (username: string) => Bluebird<UserInstance>
+ export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
+
+ export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
+}
+
+export interface UserClass {
+ isPasswordMatch: UserMethods.IsPasswordMatch,
+ toFormattedJSON: UserMethods.ToFormattedJSON,
+ hasRight: UserMethods.HasRight,
+ isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
+
+ countTotal: UserMethods.CountTotal,
+ getByUsername: UserMethods.GetByUsername,
+ listForApi: UserMethods.ListForApi,
+ loadById: UserMethods.LoadById,
+ loadByUsername: UserMethods.LoadByUsername,
+ loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
+ loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
+}
+
+export interface UserAttributes {
+ id?: number
+ password: string
+ username: string
+ email: string
+ displayNSFW?: boolean
+ role: UserRole
+ videoQuota: number
+
+ Account?: AccountInstance
+}
+
+export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
+ id: number
+ createdAt: Date
+ updatedAt: Date
+
+ isPasswordMatch: UserMethods.IsPasswordMatch
+ toFormattedJSON: UserMethods.ToFormattedJSON
+ hasRight: UserMethods.HasRight
+}
+
+export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+import { getSort, addMethodsToModel } from '../utils'
+import {
+ cryptPassword,
+ comparePassword,
+ isUserPasswordValid,
+ isUserUsernameValid,
+ isUserDisplayNSFWValid,
+ isUserVideoQuotaValid,
+ isUserRoleValid
+} from '../../helpers'
+import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
+
+import {
+ UserInstance,
+ UserAttributes,
+
+ UserMethods
+} from './user-interface'
+
+let User: Sequelize.Model<UserInstance, UserAttributes>
+let isPasswordMatch: UserMethods.IsPasswordMatch
+let hasRight: UserMethods.HasRight
+let toFormattedJSON: UserMethods.ToFormattedJSON
+let countTotal: UserMethods.CountTotal
+let getByUsername: UserMethods.GetByUsername
+let listForApi: UserMethods.ListForApi
+let loadById: UserMethods.LoadById
+let loadByUsername: UserMethods.LoadByUsername
+let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
+let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
+let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
+
+export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
+ User = sequelize.define<UserInstance, UserAttributes>('User',
+ {
+ password: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ passwordValid: value => {
+ const res = isUserPasswordValid(value)
+ if (res === false) throw new Error('Password not valid.')
+ }
+ }
+ },
+ username: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ usernameValid: value => {
+ const res = isUserUsernameValid(value)
+ if (res === false) throw new Error('Username not valid.')
+ }
+ }
+ },
+ email: {
+ type: DataTypes.STRING(400),
+ allowNull: false,
+ validate: {
+ isEmail: true
+ }
+ },
+ displayNSFW: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false,
+ validate: {
+ nsfwValid: value => {
+ const res = isUserDisplayNSFWValid(value)
+ if (res === false) throw new Error('Display NSFW is not valid.')
+ }
+ }
+ },
+ role: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ validate: {
+ roleValid: value => {
+ const res = isUserRoleValid(value)
+ if (res === false) throw new Error('Role is not valid.')
+ }
+ }
+ },
+ videoQuota: {
+ type: DataTypes.BIGINT,
+ allowNull: false,
+ validate: {
+ videoQuotaValid: value => {
+ const res = isUserVideoQuotaValid(value)
+ if (res === false) throw new Error('Video quota is not valid.')
+ }
+ }
+ }
+ },
+ {
+ indexes: [
+ {
+ fields: [ 'username' ],
+ unique: true
+ },
+ {
+ fields: [ 'email' ],
+ unique: true
+ }
+ ],
+ hooks: {
+ beforeCreate: beforeCreateOrUpdate,
+ beforeUpdate: beforeCreateOrUpdate
+ }
+ }
+ )
+
+ const classMethods = [
+ associate,
+
+ countTotal,
+ getByUsername,
+ listForApi,
+ loadById,
+ loadByUsername,
+ loadByUsernameAndPopulateChannels,
+ loadByUsernameOrEmail
+ ]
+ const instanceMethods = [
+ hasRight,
+ isPasswordMatch,
+ toFormattedJSON,
+ isAbleToUploadVideo
+ ]
+ addMethodsToModel(User, classMethods, instanceMethods)
+
+ return User
+}
+
+function beforeCreateOrUpdate (user: UserInstance) {
+ if (user.changed('password')) {
+ return cryptPassword(user.password)
+ .then(hash => {
+ user.password = hash
+ return undefined
+ })
+ }
+}
+
+// ------------------------------ METHODS ------------------------------
+
+hasRight = function (this: UserInstance, right: UserRight) {
+ return hasUserRight(this.role, right)
+}
+
+isPasswordMatch = function (this: UserInstance, password: string) {
+ return comparePassword(password, this.password)
+}
+
+toFormattedJSON = function (this: UserInstance) {
+ const json = {
+ id: this.id,
+ username: this.username,
+ email: this.email,
+ displayNSFW: this.displayNSFW,
+ role: this.role,
+ roleLabel: USER_ROLE_LABELS[this.role],
+ videoQuota: this.videoQuota,
+ createdAt: this.createdAt,
+ author: {
+ id: this.Account.id,
+ uuid: this.Account.uuid
+ }
+ }
+
+ if (Array.isArray(this.Account.VideoChannels) === true) {
+ const videoChannels = this.Account.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
+}
+
+isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
+ if (this.videoQuota === -1) return Promise.resolve(true)
+
+ return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
+ return (videoFile.size + totalBytes) < this.videoQuota
+ })
+}
+
+// ------------------------------ STATICS ------------------------------
+
+function associate (models) {
+ User.hasOne(models.Account, {
+ foreignKey: 'userId',
+ onDelete: 'cascade'
+ })
+
+ User.hasMany(models.OAuthToken, {
+ foreignKey: 'userId',
+ onDelete: 'cascade'
+ })
+}
+
+countTotal = function () {
+ return this.count()
+}
+
+getByUsername = function (username: string) {
+ const query = {
+ where: {
+ username: username
+ },
+ include: [ { model: User['sequelize'].models.Account, required: true } ]
+ }
+
+ return User.findOne(query)
+}
+
+listForApi = function (start: number, count: number, sort: string) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: [ getSort(sort) ],
+ include: [ { model: User['sequelize'].models.Account, required: true } ]
+ }
+
+ return User.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+}
+
+loadById = function (id: number) {
+ const options = {
+ include: [ { model: User['sequelize'].models.Account, required: true } ]
+ }
+
+ return User.findById(id, options)
+}
+
+loadByUsername = function (username: string) {
+ const query = {
+ where: {
+ username
+ },
+ include: [ { model: User['sequelize'].models.Account, required: true } ]
+ }
+
+ return User.findOne(query)
+}
+
+loadByUsernameAndPopulateChannels = function (username: string) {
+ const query = {
+ where: {
+ username
+ },
+ include: [
+ {
+ model: User['sequelize'].models.Account,
+ required: true,
+ include: [ User['sequelize'].models.VideoChannel ]
+ }
+ ]
+ }
+
+ return User.findOne(query)
+}
+
+loadByUsernameOrEmail = function (username: string, email: string) {
+ const query = {
+ include: [ { model: User['sequelize'].models.Account, required: true } ],
+ where: {
+ [Sequelize.Op.or]: [ { username }, { email } ]
+ }
+ }
+
+ // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
+ return (User as any).findOne(query)
+}
+
+// ---------------------------------------------------------------------------
+
+function getOriginalVideoFileTotalFromUser (user: UserInstance) {
+ // 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 "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
+ 'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' +
+ 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
+ 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
+
+ const options = {
+ bind: { userId: user.id },
+ type: Sequelize.QueryTypes.SELECT
+ }
+ return User['sequelize'].query(query, options).then(([ { total } ]) => {
+ if (total === null) return 0
+
+ return parseInt(total, 10)
+ })
+}
export * from './oauth'
export * from './pod'
export * from './request'
-export * from './user'
+export * from './account'
export * from './video'
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
-import { JobState } from '../../../shared/models/job.model'
+import { JobCategory, JobState } from '../../../shared/models/job.model'
export namespace JobMethods {
- export type ListWithLimit = (limit: number, state: JobState) => Promise<JobInstance[]>
+ export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
}
export interface JobClass {
- listWithLimit: JobMethods.ListWithLimit
+ listWithLimitByCategory: JobMethods.ListWithLimitByCategory
}
export interface JobAttributes {
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
-import { JOB_STATES } from '../../initializers'
+import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
import { addMethodsToModel } from '../utils'
import {
import { JobState } from '../../../shared/models/job.model'
let Job: Sequelize.Model<JobInstance, JobAttributes>
-let listWithLimit: JobMethods.ListWithLimit
+let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Job = sequelize.define<JobInstance, JobAttributes>('Job',
type: DataTypes.ENUM(values(JOB_STATES)),
allowNull: false
},
+ category: {
+ type: DataTypes.ENUM(values(JOB_CATEGORIES)),
+ allowNull: false
+ },
handlerName: {
type: DataTypes.STRING,
allowNull: false
}
)
- const classMethods = [ listWithLimit ]
+ const classMethods = [ listWithLimitByCategory ]
addMethodsToModel(Job, classMethods)
return Job
// ---------------------------------------------------------------------------
-listWithLimit = function (limit: number, state: JobState) {
+listWithLimitByCategory = function (limit: number, state: JobState) {
const query = {
order: [
[ 'id', 'ASC' ]
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
-import { UserModel } from '../user/user-interface'
+import { UserModel } from '../account/user-interface'
export type OAuthTokenInfo = {
refreshToken: string
export interface PodAttributes {
id?: number
host?: string
- publicKey?: string
score?: number | Sequelize.literal // Sequelize literal for 'score +' + value
- email?: string
}
export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> {
}
}
},
- publicKey: {
- type: DataTypes.STRING(5000),
- allowNull: false
- },
score: {
type: DataTypes.INTEGER,
defaultValue: FRIEND_SCORE.BASE,
isInt: true,
max: FRIEND_SCORE.MAX
}
- },
- email: {
- type: DataTypes.STRING(400),
- allowNull: false,
- validate: {
- isEmail: true
- }
}
},
{
const json = {
id: this.id,
host: this.host,
- email: this.email,
score: this.score as number,
createdAt: this.createdAt
}
+++ /dev/null
-export * from './user-video-rate-interface'
-export * from './user-interface'
+++ /dev/null
-import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-// Don't use barrel, import just what we need
-import { User as FormattedUser } from '../../../shared/models/users/user.model'
-import { ResultList } from '../../../shared/models/result-list.model'
-import { AuthorInstance } from '../video/author-interface'
-import { UserRight } from '../../../shared/models/users/user-right.enum'
-import { UserRole } from '../../../shared/models/users/user-role'
-
-export namespace UserMethods {
- export type HasRight = (this: UserInstance, right: UserRight) => boolean
- export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
-
- export type ToFormattedJSON = (this: UserInstance) => FormattedUser
- export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
-
- export type CountTotal = () => Promise<number>
-
- export type GetByUsername = (username: string) => Promise<UserInstance>
-
- export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
-
- export type LoadById = (id: number) => Promise<UserInstance>
-
- export type LoadByUsername = (username: string) => Promise<UserInstance>
- export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance>
-
- export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
-}
-
-export interface UserClass {
- isPasswordMatch: UserMethods.IsPasswordMatch,
- toFormattedJSON: UserMethods.ToFormattedJSON,
- hasRight: UserMethods.HasRight,
- isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
-
- countTotal: UserMethods.CountTotal,
- getByUsername: UserMethods.GetByUsername,
- listForApi: UserMethods.ListForApi,
- loadById: UserMethods.LoadById,
- loadByUsername: UserMethods.LoadByUsername,
- loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
- loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
-}
-
-export interface UserAttributes {
- id?: number
- password: string
- username: string
- email: string
- displayNSFW?: boolean
- role: UserRole
- videoQuota: number
-
- Author?: AuthorInstance
-}
-
-export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
- id: number
- createdAt: Date
- updatedAt: Date
-
- isPasswordMatch: UserMethods.IsPasswordMatch
- toFormattedJSON: UserMethods.ToFormattedJSON
- hasRight: UserMethods.HasRight
-}
-
-export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {}
+++ /dev/null
-import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
-
-export namespace UserVideoRateMethods {
- export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance>
-}
-
-export interface UserVideoRateClass {
- load: UserVideoRateMethods.Load
-}
-
-export interface UserVideoRateAttributes {
- type: VideoRateType
- userId: number
- videoId: number
-}
-
-export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance<UserVideoRateAttributes> {
- id: number
- createdAt: Date
- updatedAt: Date
-}
-
-export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> {}
+++ /dev/null
-/*
- User rates per video.
-*/
-import { values } from 'lodash'
-import * as Sequelize from 'sequelize'
-
-import { VIDEO_RATE_TYPES } from '../../initializers'
-
-import { addMethodsToModel } from '../utils'
-import {
- UserVideoRateInstance,
- UserVideoRateAttributes,
-
- UserVideoRateMethods
-} from './user-video-rate-interface'
-
-let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes>
-let load: UserVideoRateMethods.Load
-
-export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
- UserVideoRate = sequelize.define<UserVideoRateInstance, UserVideoRateAttributes>('UserVideoRate',
- {
- type: {
- type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
- allowNull: false
- }
- },
- {
- indexes: [
- {
- fields: [ 'videoId', 'userId', 'type' ],
- unique: true
- }
- ]
- }
- )
-
- const classMethods = [
- associate,
-
- load
- ]
- addMethodsToModel(UserVideoRate, classMethods)
-
- return UserVideoRate
-}
-
-// ------------------------------ STATICS ------------------------------
-
-function associate (models) {
- UserVideoRate.belongsTo(models.Video, {
- foreignKey: {
- name: 'videoId',
- allowNull: false
- },
- onDelete: 'CASCADE'
- })
-
- UserVideoRate.belongsTo(models.User, {
- foreignKey: {
- name: 'userId',
- allowNull: false
- },
- onDelete: 'CASCADE'
- })
-}
-
-load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) {
- const options: Sequelize.FindOptions<UserVideoRateAttributes> = {
- where: {
- userId,
- videoId
- }
- }
- if (transaction) options.transaction = transaction
-
- return UserVideoRate.findOne(options)
-}
+++ /dev/null
-import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-import { getSort, addMethodsToModel } from '../utils'
-import {
- cryptPassword,
- comparePassword,
- isUserPasswordValid,
- isUserUsernameValid,
- isUserDisplayNSFWValid,
- isUserVideoQuotaValid,
- isUserRoleValid
-} from '../../helpers'
-import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
-
-import {
- UserInstance,
- UserAttributes,
-
- UserMethods
-} from './user-interface'
-
-let User: Sequelize.Model<UserInstance, UserAttributes>
-let isPasswordMatch: UserMethods.IsPasswordMatch
-let hasRight: UserMethods.HasRight
-let toFormattedJSON: UserMethods.ToFormattedJSON
-let countTotal: UserMethods.CountTotal
-let getByUsername: UserMethods.GetByUsername
-let listForApi: UserMethods.ListForApi
-let loadById: UserMethods.LoadById
-let loadByUsername: UserMethods.LoadByUsername
-let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
-let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
-let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
-
-export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
- User = sequelize.define<UserInstance, UserAttributes>('User',
- {
- password: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- passwordValid: value => {
- const res = isUserPasswordValid(value)
- if (res === false) throw new Error('Password not valid.')
- }
- }
- },
- username: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- usernameValid: value => {
- const res = isUserUsernameValid(value)
- if (res === false) throw new Error('Username not valid.')
- }
- }
- },
- email: {
- type: DataTypes.STRING(400),
- allowNull: false,
- validate: {
- isEmail: true
- }
- },
- displayNSFW: {
- type: DataTypes.BOOLEAN,
- allowNull: false,
- defaultValue: false,
- validate: {
- nsfwValid: value => {
- const res = isUserDisplayNSFWValid(value)
- if (res === false) throw new Error('Display NSFW is not valid.')
- }
- }
- },
- role: {
- type: DataTypes.INTEGER,
- allowNull: false,
- validate: {
- roleValid: value => {
- const res = isUserRoleValid(value)
- if (res === false) throw new Error('Role is not valid.')
- }
- }
- },
- videoQuota: {
- type: DataTypes.BIGINT,
- allowNull: false,
- validate: {
- videoQuotaValid: value => {
- const res = isUserVideoQuotaValid(value)
- if (res === false) throw new Error('Video quota is not valid.')
- }
- }
- }
- },
- {
- indexes: [
- {
- fields: [ 'username' ],
- unique: true
- },
- {
- fields: [ 'email' ],
- unique: true
- }
- ],
- hooks: {
- beforeCreate: beforeCreateOrUpdate,
- beforeUpdate: beforeCreateOrUpdate
- }
- }
- )
-
- const classMethods = [
- associate,
-
- countTotal,
- getByUsername,
- listForApi,
- loadById,
- loadByUsername,
- loadByUsernameAndPopulateChannels,
- loadByUsernameOrEmail
- ]
- const instanceMethods = [
- hasRight,
- isPasswordMatch,
- toFormattedJSON,
- isAbleToUploadVideo
- ]
- addMethodsToModel(User, classMethods, instanceMethods)
-
- return User
-}
-
-function beforeCreateOrUpdate (user: UserInstance) {
- if (user.changed('password')) {
- return cryptPassword(user.password)
- .then(hash => {
- user.password = hash
- return undefined
- })
- }
-}
-
-// ------------------------------ METHODS ------------------------------
-
-hasRight = function (this: UserInstance, right: UserRight) {
- return hasUserRight(this.role, right)
-}
-
-isPasswordMatch = function (this: UserInstance, password: string) {
- return comparePassword(password, this.password)
-}
-
-toFormattedJSON = function (this: UserInstance) {
- const json = {
- id: this.id,
- username: this.username,
- email: this.email,
- displayNSFW: this.displayNSFW,
- role: this.role,
- roleLabel: USER_ROLE_LABELS[this.role],
- videoQuota: this.videoQuota,
- 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
-}
-
-isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
- if (this.videoQuota === -1) return Promise.resolve(true)
-
- return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
- return (videoFile.size + totalBytes) < this.videoQuota
- })
-}
-
-// ------------------------------ STATICS ------------------------------
-
-function associate (models) {
- User.hasOne(models.Author, {
- foreignKey: 'userId',
- onDelete: 'cascade'
- })
-
- User.hasMany(models.OAuthToken, {
- foreignKey: 'userId',
- onDelete: 'cascade'
- })
-}
-
-countTotal = function () {
- return this.count()
-}
-
-getByUsername = function (username: string) {
- const query = {
- where: {
- username: username
- },
- include: [ { model: User['sequelize'].models.Author, required: true } ]
- }
-
- return User.findOne(query)
-}
-
-listForApi = function (start: number, count: number, sort: string) {
- const query = {
- offset: start,
- limit: count,
- order: [ getSort(sort) ],
- include: [ { model: User['sequelize'].models.Author, required: true } ]
- }
-
- return User.findAndCountAll(query).then(({ rows, count }) => {
- return {
- data: rows,
- total: count
- }
- })
-}
-
-loadById = function (id: number) {
- 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)
-}
-
-loadByUsernameOrEmail = function (username: string, email: string) {
- const query = {
- include: [ { model: User['sequelize'].models.Author, required: true } ],
- where: {
- [Sequelize.Op.or]: [ { username }, { email } ]
- }
- }
-
- // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
- return (User as any).findOne(query)
-}
-
-// ---------------------------------------------------------------------------
-
-function getOriginalVideoFileTotalFromUser (user: UserInstance) {
- // 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 "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'
-
- const options = {
- bind: { userId: user.id },
- type: Sequelize.QueryTypes.SELECT
- }
- return User['sequelize'].query(query, options).then(([ { total } ]) => {
- if (total === null) return 0
-
- return parseInt(total, 10)
- })
-}
+++ /dev/null
-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 Load = (id: number) => Promise<AuthorInstance>
- export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
- export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
- export type ListOwned = () => Promise<AuthorInstance[]>
-
- export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
- export type IsOwned = (this: AuthorInstance) => boolean
-}
-
-export interface AuthorClass {
- 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<AuthorAttributes> {
- isOwned: AuthorMethods.IsOwned
- toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
-
- id: number
- createdAt: Date
- updatedAt: Date
-
- Pod: PodInstance
- VideoChannels: VideoChannelInstance[]
-}
-
-export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}
+++ /dev/null
-import * as Sequelize from 'sequelize'
-
-import { isUserUsernameValid } from '../../helpers'
-import { removeVideoAuthorToFriends } from '../../lib'
-
-import { addMethodsToModel } from '../utils'
-import {
- AuthorInstance,
- AuthorAttributes,
-
- AuthorMethods
-} from './author-interface'
-
-let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
-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<AuthorInstance, AuthorAttributes>('Author',
- {
- uuid: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- allowNull: false,
- validate: {
- isUUID: 4
- }
- },
- name: {
- type: DataTypes.STRING,
- allowNull: false,
- validate: {
- usernameValid: value => {
- const res = isUserUsernameValid(value)
- if (res === false) throw new Error('Username is not valid.')
- }
- }
- }
- },
- {
- indexes: [
- {
- fields: [ 'name' ]
- },
- {
- fields: [ 'podId' ]
- },
- {
- fields: [ 'userId' ],
- unique: true
- },
- {
- fields: [ 'name', 'podId' ],
- unique: true
- }
- ],
- hooks: { afterDestroy }
- }
- )
-
- const classMethods = [
- associate,
- loadAuthorByPodAndUUID,
- load,
- loadByUUID,
- listOwned
- ]
- const instanceMethods = [
- isOwned,
- toAddRemoteJSON
- ]
- addMethodsToModel(Author, classMethods, instanceMethods)
-
- return Author
-}
-
-// ---------------------------------------------------------------------------
-
-function associate (models) {
- Author.belongsTo(models.Pod, {
- foreignKey: {
- name: 'podId',
- allowNull: true
- },
- onDelete: 'cascade'
- })
-
- Author.belongsTo(models.User, {
- foreignKey: {
- name: 'userId',
- allowNull: true
- },
- onDelete: 'cascade'
- })
-
- Author.hasMany(models.VideoChannel, {
- foreignKey: {
- name: 'authorId',
- allowNull: false
- },
- onDelete: 'cascade',
- hooks: true
- })
-}
-
-function afterDestroy (author: AuthorInstance) {
- if (author.isOwned()) {
- const removeVideoAuthorToFriendsParams = {
- uuid: author.uuid
- }
-
- return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams)
- }
-
- 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<AuthorAttributes> = {
- where: {
- podId: null
- }
- }
-
- return Author.findAll(query)
-}
-
-load = function (id: number) {
- return Author.findById(id)
-}
-
-loadByUUID = function (uuid: string) {
- const query: Sequelize.FindOptions<AuthorAttributes> = {
- where: {
- uuid
- }
- }
-
- return Author.findOne(query)
-}
-
-loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
- const query: Sequelize.FindOptions<AuthorAttributes> = {
- where: {
- podId,
- uuid
- },
- transaction
- }
-
- return Author.find(query)
-}
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
-import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared'
+import { ResultList } 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'
+import { AccountInstance } from '../account/account-interface'
+import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
export namespace VideoChannelMethods {
export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
- export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData
- export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
+ export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject
export type IsOwned = (this: VideoChannelInstance) => boolean
- export type CountByAuthor = (authorId: number) => Promise<number>
+ export type CountByAccount = (accountId: number) => Promise<number>
export type ListOwned = () => Promise<VideoChannelInstance[]>
export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
- export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance>
- export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> >
- export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance>
- export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance>
+ export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance>
+ export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> >
+ export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance>
+ export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance>
export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
- export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance>
+ export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
}
export interface VideoChannelClass {
- countByAuthor: VideoChannelMethods.CountByAuthor
+ countByAccount: VideoChannelMethods.CountByAccount
listForApi: VideoChannelMethods.ListForApi
- listByAuthor: VideoChannelMethods.ListByAuthor
+ listByAccount: VideoChannelMethods.ListByAccount
listOwned: VideoChannelMethods.ListOwned
- loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
+ loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
loadByUUID: VideoChannelMethods.LoadByUUID
loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
- loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
- loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
- loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
+ loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
+ loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
+ loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
}
export interface VideoChannelAttributes {
name: string
description: string
remote: boolean
+ url: string
- Author?: AuthorInstance
+ Account?: AccountInstance
Videos?: VideoInstance[]
}
isOwned: VideoChannelMethods.IsOwned
toFormattedJSON: VideoChannelMethods.ToFormattedJSON
- toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
- toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
+ toActivityPubObject: VideoChannelMethods.ToActivityPubObject
}
export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
-let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
-let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
+let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
let isOwned: VideoChannelMethods.IsOwned
-let countByAuthor: VideoChannelMethods.CountByAuthor
+let countByAccount: VideoChannelMethods.CountByAccount
let listOwned: VideoChannelMethods.ListOwned
let listForApi: VideoChannelMethods.ListForApi
-let listByAuthor: VideoChannelMethods.ListByAuthor
-let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
+let listByAccount: VideoChannelMethods.ListByAccount
+let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
let loadByUUID: VideoChannelMethods.LoadByUUID
-let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
-let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
+let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
+let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
-let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
+let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
+ },
+ url: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ isUrl: true
+ }
}
},
{
indexes: [
{
- fields: [ 'authorId' ]
+ fields: [ 'accountId' ]
}
],
hooks: {
associate,
listForApi,
- listByAuthor,
+ listByAccount,
listOwned,
- loadByIdAndAuthor,
- loadAndPopulateAuthor,
- loadByUUIDAndPopulateAuthor,
+ loadByIdAndAccount,
+ loadAndPopulateAccount,
+ loadByUUIDAndPopulateAccount,
loadByUUID,
loadByHostAndUUID,
- loadAndPopulateAuthorAndVideos,
- countByAuthor
+ loadAndPopulateAccountAndVideos,
+ countByAccount
]
const instanceMethods = [
isOwned,
toFormattedJSON,
- toAddRemoteJSON,
- toUpdateRemoteJSON
+ toActivityPubObject,
]
addMethodsToModel(VideoChannel, classMethods, instanceMethods)
updatedAt: this.updatedAt
}
- if (this.Author !== undefined) {
+ if (this.Account !== undefined) {
json['owner'] = {
- name: this.Author.name,
- uuid: this.Author.uuid
+ name: this.Account.name,
+ uuid: this.Account.uuid
}
}
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) {
+toActivityPubObject = function (this: VideoChannelInstance) {
const json = {
uuid: this.uuid,
name: this.name,
description: this.description,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
- ownerUUID: this.Author.uuid
+ ownerUUID: this.Account.uuid
}
return json
// ------------------------------ STATICS ------------------------------
function associate (models) {
- VideoChannel.belongsTo(models.Author, {
+ VideoChannel.belongsTo(models.Account, {
foreignKey: {
- name: 'authorId',
+ name: 'accountId',
allowNull: false
},
onDelete: 'CASCADE'
return undefined
}
-countByAuthor = function (authorId: number) {
+countByAccount = function (accountId: number) {
const query = {
where: {
- authorId
+ accountId
}
}
where: {
remote: false
},
- include: [ VideoChannel['sequelize'].models.Author ]
+ include: [ VideoChannel['sequelize'].models.Account ]
}
return VideoChannel.findAll(query)
order: [ getSort(sort) ],
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
required: true,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
}
})
}
-listByAuthor = function (authorId: number) {
+listByAccount = function (accountId: number) {
const query = {
order: [ getSort('createdAt') ],
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
where: {
- id: authorId
+ id: accountId
},
required: true,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
},
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
include: [
{
model: VideoChannel['sequelize'].models.Pod,
return VideoChannel.findOne(query)
}
-loadByIdAndAuthor = function (id: number, authorId: number) {
+loadByIdAndAccount = function (id: number, accountId: number) {
const options = {
where: {
id,
- authorId
+ accountId
},
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
}
]
return VideoChannel.findOne(options)
}
-loadAndPopulateAuthor = function (id: number) {
+loadAndPopulateAccount = function (id: number) {
const options = {
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
}
]
return VideoChannel.findById(id, options)
}
-loadByUUIDAndPopulateAuthor = function (uuid: string) {
+loadByUUIDAndPopulateAccount = function (uuid: string) {
const options = {
where: {
uuid
},
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
}
]
return VideoChannel.findOne(options)
}
-loadAndPopulateAuthorAndVideos = function (id: number) {
+loadAndPopulateAccountAndVideos = function (id: number) {
const options = {
include: [
{
- model: VideoChannel['sequelize'].models.Author,
+ model: VideoChannel['sequelize'].models.Account,
include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
},
VideoChannel['sequelize'].models.Video
import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
+import * as Bluebird from 'bluebird'
import { TagAttributes, TagInstance } from './tag-interface'
import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
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'
+import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
export namespace VideoMethods {
export type GetThumbnailName = (this: VideoInstance) => string
export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
- export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData>
- export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
+ export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject
export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
export type GetPreviewPath = (this: VideoInstance) => string
export type GetDescriptionPath = (this: VideoInstance) => string
export type GetTruncatedDescription = (this: VideoInstance) => string
+ export type GetCategoryLabel = (this: VideoInstance) => string
+ export type GetLicenceLabel = (this: VideoInstance) => string
+ export type GetLanguageLabel = (this: VideoInstance) => string
// Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
- export type List = () => Promise<VideoInstance[]>
- export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]>
- export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]>
+ export type List = () => Bluebird<VideoInstance[]>
+ export type ListOwnedAndPopulateAccountAndTags = () => Bluebird<VideoInstance[]>
+ export type ListOwnedByAccount = (account: string) => Bluebird<VideoInstance[]>
- export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
- export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
- export type SearchAndPopulateAuthorAndPodAndTags = (
+ export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
+ export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
+ export type SearchAndPopulateAccountAndPodAndTags = (
value: string,
field: string,
start: number,
count: number,
sort: string
- ) => Promise< ResultList<VideoInstance> >
+ ) => Bluebird< ResultList<VideoInstance> >
- export type Load = (id: number) => Promise<VideoInstance>
- export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
- export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
- export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
- export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
- export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
- export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
+ export type Load = (id: number) => Bluebird<VideoInstance>
+ export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
+ export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
+ export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
+ export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
+ export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
+ export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
+ export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
export type RemovePreview = (this: VideoInstance) => Promise<void>
list: VideoMethods.List
listForApi: VideoMethods.ListForApi
listUserVideosForApi: VideoMethods.ListUserVideosForApi
- listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
- listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
+ listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
+ listOwnedByAccount: VideoMethods.ListOwnedByAccount
load: VideoMethods.Load
- loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
- loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
+ loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
+ loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
loadByUUID: VideoMethods.LoadByUUID
+ loadByUrl: VideoMethods.LoadByUrl
loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
- loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
- searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
+ loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
+ searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
}
export interface VideoAttributes {
likes?: number
dislikes?: number
remote: boolean
+ url: string
+ parentId?: number
channelId?: number
VideoChannel?: VideoChannelInstance
removePreview: VideoMethods.RemovePreview
removeThumbnail: VideoMethods.RemoveThumbnail
removeTorrent: VideoMethods.RemoveTorrent
- toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
+ toActivityPubObject: VideoMethods.ToActivityPubObject
toFormattedJSON: VideoMethods.ToFormattedJSON
toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
- toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
getEmbedPath: VideoMethods.GetEmbedPath
getDescriptionPath: VideoMethods.GetDescriptionPath
getTruncatedDescription: VideoMethods.GetTruncatedDescription
+ getCategoryLabel: VideoMethods.GetCategoryLabel
+ getLicenceLabel: VideoMethods.GetLicenceLabel
+ getLanguageLabel: VideoMethods.GetLanguageLabel
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
}
export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
+
import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
import { TagInstance } from './tag-interface'
import {
VideoMethods
} from './video-interface'
+import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile
let isOwned: VideoMethods.IsOwned
let toFormattedJSON: VideoMethods.ToFormattedJSON
let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
-let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
-let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
+let toActivityPubObject: VideoMethods.ToActivityPubObject
let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
let createPreview: VideoMethods.CreatePreview
let getEmbedPath: VideoMethods.GetEmbedPath
let getDescriptionPath: VideoMethods.GetDescriptionPath
let getTruncatedDescription: VideoMethods.GetTruncatedDescription
+let getCategoryLabel: VideoMethods.GetCategoryLabel
+let getLicenceLabel: VideoMethods.GetLicenceLabel
+let getLanguageLabel: VideoMethods.GetLanguageLabel
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let list: VideoMethods.List
let listForApi: VideoMethods.ListForApi
let listUserVideosForApi: VideoMethods.ListUserVideosForApi
let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
-let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
-let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
+let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
+let listOwnedByAccount: VideoMethods.ListOwnedByAccount
let load: VideoMethods.Load
let loadByUUID: VideoMethods.LoadByUUID
+let loadByUrl: VideoMethods.LoadByUrl
let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
-let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
-let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
-let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
-let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
+let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
+let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
+let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
+let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
let removeThumbnail: VideoMethods.RemoveThumbnail
let removePreview: VideoMethods.RemovePreview
let removeFile: VideoMethods.RemoveFile
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
+ },
+ url: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ validate: {
+ isUrl: true
+ }
}
},
{
},
{
fields: [ 'channelId' ]
+ },
+ {
+ fields: [ 'parentId' ]
}
],
hooks: {
list,
listForApi,
listUserVideosForApi,
- listOwnedAndPopulateAuthorAndTags,
- listOwnedByAuthor,
+ listOwnedAndPopulateAccountAndTags,
+ listOwnedByAccount,
load,
- loadAndPopulateAuthor,
- loadAndPopulateAuthorAndPodAndTags,
+ loadAndPopulateAccount,
+ loadAndPopulateAccountAndPodAndTags,
loadByHostAndUUID,
loadByUUID,
loadLocalVideoByUUID,
- loadByUUIDAndPopulateAuthorAndPodAndTags,
- searchAndPopulateAuthorAndPodAndTags
+ loadByUUIDAndPopulateAccountAndPodAndTags,
+ searchAndPopulateAccountAndPodAndTags
]
const instanceMethods = [
createPreview,
removePreview,
removeThumbnail,
removeTorrent,
- toAddRemoteJSON,
+ toActivityPubObject,
toFormattedJSON,
toFormattedDetailsJSON,
- toUpdateRemoteJSON,
optimizeOriginalVideofile,
transcodeOriginalVideofile,
getOriginalFileHeight,
getEmbedPath,
getTruncatedDescription,
- getDescriptionPath
+ getDescriptionPath,
+ getCategoryLabel,
+ getLicenceLabel,
+ getLanguageLabel
]
addMethodsToModel(Video, classMethods, instanceMethods)
onDelete: 'cascade'
})
+ Video.belongsTo(models.VideoChannel, {
+ foreignKey: {
+ name: 'parentId',
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
+
Video.belongsToMany(models.Tag, {
foreignKey: 'videoId',
through: models.VideoTag,
return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
}
-createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
+createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
const options = {
announceList: [
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
]
}
- return createTorrentPromise(this.getVideoFilePath(videoFile), options)
- .then(torrent => {
- const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
- logger.info('Creating torrent %s.', filePath)
+ const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
- return writeFilePromise(filePath, torrent).then(() => torrent)
- })
- .then(torrent => {
- const parsedTorrent = parseTorrent(torrent)
+ const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
+ logger.info('Creating torrent %s.', filePath)
- videoFile.infoHash = parsedTorrent.infoHash
- })
+ await writeFilePromise(filePath, torrent)
+
+ const parsedTorrent = parseTorrent(torrent)
+ videoFile.infoHash = parsedTorrent.infoHash
}
getEmbedPath = function (this: VideoInstance) {
toFormattedJSON = function (this: VideoInstance) {
let podHost
- if (this.VideoChannel.Author.Pod) {
- podHost = this.VideoChannel.Author.Pod.host
+ if (this.VideoChannel.Account.Pod) {
+ podHost = this.VideoChannel.Account.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,
+ categoryLabel: this.getCategoryLabel(),
licence: this.licence,
- licenceLabel,
+ licenceLabel: this.getLicenceLabel(),
language: this.language,
- languageLabel,
+ languageLabel: this.getLanguageLabel(),
nsfw: this.nsfw,
description: this.getTruncatedDescription(),
podHost,
isLocal: this.isOwned(),
- author: this.VideoChannel.Author.name,
+ account: this.VideoChannel.Account.name,
duration: this.duration,
views: this.views,
likes: this.likes,
return Object.assign(formattedJson, detailsJson)
}
-toAddRemoteJSON = function (this: VideoInstance) {
- // Get thumbnail data to send to the other pod
- const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
+toActivityPubObject = function (this: VideoInstance) {
+ const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
- return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
- const remoteVideo = {
- uuid: this.uuid,
- name: this.name,
- category: this.category,
- licence: this.licence,
- language: this.language,
- nsfw: this.nsfw,
- truncatedDescription: this.getTruncatedDescription(),
- channelUUID: this.VideoChannel.uuid,
- duration: this.duration,
- thumbnailData: thumbnailData.toString('binary'),
- tags: map<TagInstance, string>(this.Tags, 'name'),
- createdAt: this.createdAt,
- updatedAt: this.updatedAt,
- views: this.views,
- likes: this.likes,
- dislikes: this.dislikes,
- privacy: this.privacy,
- files: []
- }
+ const tag = this.Tags.map(t => ({
+ type: 'Hashtag',
+ name: t.name
+ }))
+
+ const url = []
+ for (const file of this.VideoFiles) {
+ url.push({
+ type: 'Link',
+ mimeType: 'video/' + file.extname,
+ url: getVideoFileUrl(this, file, baseUrlHttp),
+ width: file.resolution,
+ size: file.size
+ })
- this.VideoFiles.forEach(videoFile => {
- remoteVideo.files.push({
- infoHash: videoFile.infoHash,
- resolution: videoFile.resolution,
- extname: videoFile.extname,
- size: videoFile.size
- })
+ url.push({
+ type: 'Link',
+ mimeType: 'application/x-bittorrent',
+ url: getTorrentUrl(this, file, baseUrlHttp),
+ width: file.resolution
})
- return remoteVideo
- })
-}
+ url.push({
+ type: 'Link',
+ mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
+ url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
+ width: file.resolution
+ })
+ }
-toUpdateRemoteJSON = function (this: VideoInstance) {
- const json = {
- uuid: this.uuid,
+ const videoObject: VideoTorrentObject = {
+ type: 'Video',
name: this.name,
- category: this.category,
- licence: this.licence,
- language: this.language,
- nsfw: this.nsfw,
- truncatedDescription: this.getTruncatedDescription(),
- duration: this.duration,
- tags: map<TagInstance, string>(this.Tags, 'name'),
- createdAt: this.createdAt,
- updatedAt: this.updatedAt,
+ // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
+ duration: 'PT' + this.duration + 'S',
+ uuid: this.uuid,
+ tag,
+ category: {
+ id: this.category,
+ label: this.getCategoryLabel()
+ },
+ licence: {
+ id: this.licence,
+ name: this.getLicenceLabel()
+ },
+ language: {
+ id: this.language,
+ name: this.getLanguageLabel()
+ },
views: this.views,
- likes: this.likes,
- dislikes: this.dislikes,
- privacy: this.privacy,
- files: []
+ nsfw: this.nsfw,
+ published: this.createdAt,
+ updated: this.updatedAt,
+ mediaType: 'text/markdown',
+ content: this.getTruncatedDescription(),
+ icon: {
+ type: 'Image',
+ url: getThumbnailUrl(this, baseUrlHttp),
+ mediaType: 'image/jpeg',
+ width: THUMBNAILS_SIZE.width,
+ height: THUMBNAILS_SIZE.height
+ },
+ url
}
- this.VideoFiles.forEach(videoFile => {
- json.files.push({
- infoHash: videoFile.infoHash,
- resolution: videoFile.resolution,
- extname: videoFile.extname,
- size: videoFile.size
- })
- })
-
- return json
+ return videoObject
}
getTruncatedDescription = function (this: VideoInstance) {
return truncate(this.description, options)
}
-optimizeOriginalVideofile = function (this: VideoInstance) {
+optimizeOriginalVideofile = async function (this: VideoInstance) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4'
const inputVideoFile = this.getOriginalFile()
outputPath: videoOutputPath
}
- return transcode(transcodeOptions)
- .then(() => {
- return unlinkPromise(videoInputPath)
- })
- .then(() => {
- // Important to do this before getVideoFilename() to take in account the new file extension
- inputVideoFile.set('extname', newExtname)
+ try {
+ // Could be very long!
+ await transcode(transcodeOptions)
- return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
- })
- .then(() => {
- return statPromise(this.getVideoFilePath(inputVideoFile))
- })
- .then(stats => {
- return inputVideoFile.set('size', stats.size)
- })
- .then(() => {
- return this.createTorrentAndSetInfoHash(inputVideoFile)
- })
- .then(() => {
- return inputVideoFile.save()
- })
- .then(() => {
- return undefined
- })
- .catch(err => {
- // Auto destruction...
- this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
+ await unlinkPromise(videoInputPath)
- throw err
- })
+ // Important to do this before getVideoFilename() to take in account the new file extension
+ inputVideoFile.set('extname', newExtname)
+
+ await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
+ const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
+
+ inputVideoFile.set('size', stats.size)
+
+ await this.createTorrentAndSetInfoHash(inputVideoFile)
+ await inputVideoFile.save()
+
+ } catch (err) {
+ // Auto destruction...
+ this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
+
+ throw err
+ }
}
-transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
+transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const extname = '.mp4'
outputPath: videoOutputPath,
resolution
}
- return transcode(transcodeOptions)
- .then(() => {
- return statPromise(videoOutputPath)
- })
- .then(stats => {
- newVideoFile.set('size', stats.size)
- return undefined
- })
- .then(() => {
- return this.createTorrentAndSetInfoHash(newVideoFile)
- })
- .then(() => {
- return newVideoFile.save()
- })
- .then(() => {
- return this.VideoFiles.push(newVideoFile)
- })
- .then(() => undefined)
+ await transcode(transcodeOptions)
+
+ const stats = await statPromise(videoOutputPath)
+
+ newVideoFile.set('size', stats.size)
+
+ await this.createTorrentAndSetInfoHash(newVideoFile)
+
+ await newVideoFile.save()
+
+ this.VideoFiles.push(newVideoFile)
}
getOriginalFileHeight = function (this: VideoInstance) {
return `/api/${API_VERSION}/videos/${this.uuid}/description`
}
+getCategoryLabel = function (this: VideoInstance) {
+ let categoryLabel = VIDEO_CATEGORIES[this.category]
+
+ // Maybe our pod is not up to date and there are new categories since our version
+ if (!categoryLabel) categoryLabel = 'Misc'
+
+ return categoryLabel
+}
+
+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 licenceLabel
+}
+
+getLanguageLabel = function (this: VideoInstance) {
+ // Language is an optional attribute
+ let languageLabel = VIDEO_LANGUAGES[this.language]
+ if (!languageLabel) languageLabel = 'Unknown'
+
+ return languageLabel
+}
+
removeThumbnail = function (this: VideoInstance) {
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
return unlinkPromise(thumbnailPath)
required: true,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
where: {
userId
},
model: Video['sequelize'].models.VideoChannel,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
include: [
{
model: Video['sequelize'].models.Pod,
model: Video['sequelize'].models.VideoChannel,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
include: [
{
model: Video['sequelize'].models.Pod,
return Video.findOne(query)
}
-listOwnedAndPopulateAuthorAndTags = function () {
+listOwnedAndPopulateAccountAndTags = function () {
const query = {
where: {
remote: false
Video['sequelize'].models.VideoFile,
{
model: Video['sequelize'].models.VideoChannel,
- include: [ Video['sequelize'].models.Author ]
+ include: [ Video['sequelize'].models.Account ]
},
Video['sequelize'].models.Tag
]
return Video.findAll(query)
}
-listOwnedByAuthor = function (author: string) {
+listOwnedByAccount = function (account: string) {
const query = {
where: {
remote: false
model: Video['sequelize'].models.VideoChannel,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
where: {
- name: author
+ name: account
}
}
]
return Video.findOne(query)
}
-loadAndPopulateAuthor = function (id: number) {
+loadAndPopulateAccount = function (id: number) {
const options = {
include: [
Video['sequelize'].models.VideoFile,
{
model: Video['sequelize'].models.VideoChannel,
- include: [ Video['sequelize'].models.Author ]
+ include: [ Video['sequelize'].models.Account ]
}
]
}
return Video.findById(id, options)
}
-loadAndPopulateAuthorAndPodAndTags = function (id: number) {
+loadAndPopulateAccountAndPodAndTags = function (id: number) {
const options = {
include: [
{
model: Video['sequelize'].models.VideoChannel,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
}
]
return Video.findById(id, options)
}
-loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
+loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
const options = {
where: {
uuid
model: Video['sequelize'].models.VideoChannel,
include: [
{
- model: Video['sequelize'].models.Author,
+ model: Video['sequelize'].models.Account,
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
}
]
return Video.findOne(options)
}
-searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
+searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
const podInclude: Sequelize.IncludeOptions = {
model: Video['sequelize'].models.Pod,
required: false
}
- const authorInclude: Sequelize.IncludeOptions = {
- model: Video['sequelize'].models.Author,
+ const accountInclude: Sequelize.IncludeOptions = {
+ model: Video['sequelize'].models.Account,
include: [ podInclude ]
}
const videoChannelInclude: Sequelize.IncludeOptions = {
model: Video['sequelize'].models.VideoChannel,
- include: [ authorInclude ],
+ include: [ accountInclude ],
required: true
}
}
}
podInclude.required = true
- } else if (field === 'author') {
- authorInclude.where = {
+ } else if (field === 'account') {
+ accountInclude.where = {
name: {
[Sequelize.Op.iLike]: '%' + value + '%'
}
baseUrlHttp = CONFIG.WEBSERVER.URL
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
} else {
- baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
- baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
+ baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
+ baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
}
return { baseUrlHttp, baseUrlWs }
}
+function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
+ return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
+}
+
function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
}
--- /dev/null
+import {
+ VideoChannelObject,
+ VideoTorrentObject
+} from './objects'
+import { ActivityPubSignature } from './activitypub-signature'
+
+export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag
+
+// Flag -> report abuse
+export type ActivityType = 'Create' | 'Update' | 'Flag'
+
+export interface BaseActivity {
+ '@context'?: any[]
+ id: string
+ to: string[]
+ actor: string
+ type: ActivityType
+ signature: ActivityPubSignature
+}
+
+export interface ActivityCreate extends BaseActivity {
+ type: 'Create'
+ object: VideoTorrentObject | VideoChannelObject
+}
+
+export interface ActivityUpdate extends BaseActivity {
+ type: 'Update'
+ object: VideoTorrentObject | VideoChannelObject
+}
+
+export interface ActivityFlag extends BaseActivity {
+ type: 'Flag'
+ object: string
+}
--- /dev/null
+export interface ActivityPubActor {
+ '@context': any[]
+ type: 'Person' | 'Application'
+ id: string
+ following: string
+ followers: string
+ inbox: string
+ outbox: string
+ preferredUsername: string
+ url: string
+ name: string
+ endpoints: {
+ sharedInbox: string
+ }
+
+ uuid: string
+ publicKey: {
+ id: string
+ owner: string
+ publicKeyPem: string
+ }
+
+ // Not used
+ // summary: string
+ // icon: string[]
+ // liked: string
+}
--- /dev/null
+import { Activity } from './activity'
+
+export interface ActivityPubCollection {
+ '@context': string[]
+ type: 'Collection' | 'CollectionPage'
+ totalItems: number
+ partOf?: string
+ items: Activity[]
+}
--- /dev/null
+import { Activity } from './activity'
+
+export interface ActivityPubOrderedCollection {
+ '@context': string[]
+ type: 'OrderedCollection' | 'OrderedCollectionPage'
+ totalItems: number
+ partOf?: string
+ orderedItems: Activity[]
+}
--- /dev/null
+import { Activity } from './activity'
+import { ActivityPubCollection } from './activitypub-collection'
+import { ActivityPubOrderedCollection } from './activitypub-ordered-collection'
+
+export type RootActivity = Activity | ActivityPubCollection | ActivityPubOrderedCollection
--- /dev/null
+export interface ActivityPubSignature {
+ type: 'GraphSignature2012'
+ created: Date,
+ creator: string
+ signatureValue: string
+}
--- /dev/null
+export * from './activity'
+export * from './activitypub-actor'
+export * from './activitypub-collection'
+export * from './activitypub-ordered-collection'
+export * from './activitypub-root'
+export * from './activitypub-signature'
+export * from './objects'
+export * from './webfinger'
--- /dev/null
+export interface ActivityIdentifierObject {
+ identifier: string
+ name: string
+}
+
+export interface ActivityTagObject {
+ type: 'Hashtag'
+ name: string
+}
+
+export interface ActivityIconObject {
+ type: 'Image'
+ url: string
+ mediaType: 'image/jpeg'
+ width: number
+ height: number
+}
+
+export interface ActivityUrlObject {
+ type: 'Link'
+ mimeType: 'video/mp4' | 'video/webm' | 'application/x-bittorrent' | 'application/x-bittorrent;x-scheme-handler/magnet'
+ url: string
+ width: number
+ size?: number
+}
--- /dev/null
+export * from './common-objects'
+export * from './video-channel-object'
+export * from './video-torrent-object'
--- /dev/null
+import { ActivityIdentifierObject } from './common-objects'
+
+export interface VideoChannelObject {
+ type: 'VideoChannel'
+ name: string
+ content: string
+ uuid: ActivityIdentifierObject
+}
--- /dev/null
+import {
+ ActivityIconObject,
+ ActivityIdentifierObject,
+ ActivityTagObject,
+ ActivityUrlObject
+} from './common-objects'
+
+export interface VideoTorrentObject {
+ type: 'Video'
+ name: string
+ duration: string
+ uuid: string
+ tag: ActivityTagObject[]
+ category: ActivityIdentifierObject
+ licence: ActivityIdentifierObject
+ language: ActivityIdentifierObject
+ views: number
+ nsfw: boolean
+ published: Date
+ updated: Date
+ mediaType: 'text/markdown'
+ content: string
+ icon: ActivityIconObject
+ url: ActivityUrlObject[]
+}
--- /dev/null
+export interface WebFingerData {
+ subject: string
+ aliases: string[]
+ links: {
+ rel: 'self'
+ type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ href: string
+ }[]
+}
+export * from './activitypub'
export * from './pods'
export * from './users'
export * from './videos'
export type JobState = 'pending' | 'processing' | 'error' | 'success'
+export type JobCategory = 'transcoding' | 'http-request'
export interface Video {
id: number
uuid: string
- author: string
+ account: string
createdAt: Date | string
updatedAt: Date | string
categoryLabel: string
"@types/node" "*"
"@types/parse-torrent-file" "*"
+"@types/pem@^1.9.3":
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.3.tgz#0c864c8b79e43fef6367db895f60fd1edd10e86c"
+
"@types/request@^2.0.3":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.0.7.tgz#a2aa5a57317c21971d9b024e393091ab2c99ab98"
version "1.3.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
+bitcore-lib@^0.13.7:
+ version "0.13.19"
+ resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc"
+ dependencies:
+ bn.js "=2.0.4"
+ bs58 "=2.0.0"
+ buffer-compare "=1.0.0"
+ elliptic "=3.0.3"
+ inherits "=2.0.1"
+ lodash "=3.10.1"
+
+"bitcore-message@github:CoMakery/bitcore-message#dist":
+ version "1.0.2"
+ resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
+ dependencies:
+ bitcore-lib "^0.13.7"
+
bitfield@^1.0.1, bitfield@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-1.1.2.tgz#a5477f00e33f2a76edc209aaf26bf09394a378cf"
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+bn.js@=2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
+
+bn.js@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
+
bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
preserve "^0.2.0"
repeat-element "^1.1.2"
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
browser-stdout@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
version "1.0.1"
resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
+bs58@=2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
+
buffer-alloc-unsafe@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.0.0.tgz#474aa88f34e7bc75fa311d2e6457409c5846c3fe"
+buffer-compare@=1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
+
buffer-equals@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5"
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
+charenc@~0.0.1:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+
check-error@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
version "2.6.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
+commander@~2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
compact2string@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/compact2string/-/compact2string-1.4.0.tgz#a99cd96ea000525684b269683ae2222d6eea7b49"
shebang-command "^1.2.0"
which "^1.2.9"
+crypt@~0.0.1:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+elliptic@=3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
+ dependencies:
+ bn.js "^2.0.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ inherits "^2.0.1"
+
encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
es6-symbol "~3.1.1"
event-emitter "~0.3.5"
+es6-promise@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.3.0.tgz#96edb9f2fdb01995822b263dd8aadab6748181bc"
+
es6-promise@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+es6-promise@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.0.1.tgz#ccc4963e679f0ca9fb187c777b9e583d3c7573c2"
+
+es6-promise@~4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
+
es6-set@~0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
growl@1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
dependencies:
function-bind "^1.0.2"
+hash.js@^1.0.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
hawk@3.1.3, hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+inherits@=2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
ini@^1.3.4, ini@~1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2"
-is-buffer@^1.1.5:
+is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+jsonld-signatures@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-1.2.1.tgz#493df5df9cd3a9f1b1cb296bbd3d081679f20ca8"
+ dependencies:
+ async "^1.5.2"
+ bitcore-message "github:CoMakery/bitcore-message#dist"
+ commander "~2.9.0"
+ es6-promise "~4.0.5"
+ jsonld "0.4.3"
+ node-forge "~0.6.45"
+
+jsonld@0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.3.tgz#0bbc929190064d6650a5af5876e1bfdf0ed288f3"
+ dependencies:
+ es6-promise "~2.0.1"
+ pkginfo "~0.3.0"
+ request "^2.61.0"
+ xmldom "0.1.19"
+
+jsonld@^0.4.12:
+ version "0.4.12"
+ resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.12.tgz#a02f205d5341414df1b6d8414f1b967a712073e8"
+ dependencies:
+ es6-promise "^2.0.0"
+ pkginfo "~0.4.0"
+ request "^2.61.0"
+ xmldom "0.1.19"
+
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+lodash@=3.10.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+
lowercase-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+md5@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
+ dependencies:
+ charenc "~0.0.1"
+ crypt "~0.0.1"
+ is-buffer "~1.1.1"
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
version "1.0.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
semver "^5.4.1"
+node-forge@~0.6.45:
+ version "0.6.49"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.49.tgz#f1ee95d5d74623938fe19d698aa5a26d54d2f60f"
+
node-pre-gyp@0.6.36:
version "0.6.36"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-openssl-wrapper@^0.3.4:
- version "0.3.4"
- resolved "https://registry.yarnpkg.com/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz#c01ec98e4dcd2b5dfe0b693f31827200e3b81b07"
-
optionator@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-os-tmpdir@^1.0.0:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
dependencies:
through "~2.3"
+pem@^1.12.3:
+ version "1.12.3"
+ resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.3.tgz#b1fb5c8b79da8d18146c27fee79b0d4ddf9905b3"
+ dependencies:
+ md5 "^2.2.1"
+ os-tmpdir "^1.0.1"
+ safe-buffer "^5.1.1"
+ which "^1.2.4"
+
performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
dependencies:
find-up "^1.0.0"
+pkginfo@~0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
+
+pkginfo@~0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
+
pluralize@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
tunnel-agent "^0.6.0"
uuid "^3.0.0"
-request@^2.81.0:
+request@^2.61.0, request@^2.81.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
dependencies:
pump "^1.0.1"
range-slice-stream "^1.2.0"
+webfinger.js@^2.6.6:
+ version "2.6.6"
+ resolved "https://registry.yarnpkg.com/webfinger.js/-/webfinger.js-2.6.6.tgz#52ebdc85da8c8fb6beb690e8e32594c99d2ff4ae"
+ dependencies:
+ xhr2 "^0.1.4"
+
webtorrent@^0.98.0:
version "0.98.20"
resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.98.20.tgz#f335869185a64447b6fe730c3c66265620b8c14a"
xtend "^4.0.1"
zero-fill "^2.2.3"
-which@^1.1.1, which@^1.2.9:
+which@^1.1.1, which@^1.2.4, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+xhr2@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
+
+xmldom@0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
+
xtend@4.0.1, xtend@^4.0.0, xtend@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"