Begin activitypub
authorChocobozzz <florian.bigard@gmail.com>
Thu, 9 Nov 2017 16:51:58 +0000 (17:51 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:51 +0000 (19:40 +0100)
103 files changed:
package.json
server/controllers/activitypub/client.ts [new file with mode: 0644]
server/controllers/activitypub/inbox.ts [new file with mode: 0644]
server/controllers/activitypub/index.ts [new file with mode: 0644]
server/controllers/activitypub/pods.ts [new file with mode: 0644]
server/controllers/activitypub/videos.ts [new file with mode: 0644]
server/controllers/api/remote/index.ts [deleted file]
server/controllers/api/remote/pods.ts [deleted file]
server/controllers/api/remote/videos.ts [deleted file]
server/helpers/activitypub.ts [new file with mode: 0644]
server/helpers/core-utils.ts
server/helpers/custom-validators/activitypub/account.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/index.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/misc.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/signature.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/videos.ts [new file with mode: 0644]
server/helpers/custom-validators/index.ts
server/helpers/custom-validators/remote/index.ts [deleted file]
server/helpers/custom-validators/remote/videos.ts [deleted file]
server/helpers/ffmpeg-utils.ts
server/helpers/index.ts
server/helpers/peertube-crypto.ts
server/helpers/requests.ts
server/helpers/webfinger.ts [new file with mode: 0644]
server/initializers/checker.ts
server/initializers/constants.ts
server/initializers/database.ts
server/lib/activitypub/index.ts [new file with mode: 0644]
server/lib/activitypub/process-create.ts [new file with mode: 0644]
server/lib/activitypub/process-flag.ts [new file with mode: 0644]
server/lib/activitypub/process-update.ts [new file with mode: 0644]
server/lib/activitypub/send-request.ts [new file with mode: 0644]
server/lib/index.ts
server/lib/jobs/handlers/index.ts [deleted file]
server/lib/jobs/handlers/video-file-optimizer.ts [deleted file]
server/lib/jobs/handlers/video-file-transcoder.ts [deleted file]
server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts [new file with mode: 0644]
server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts [new file with mode: 0644]
server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts [new file with mode: 0644]
server/lib/jobs/http-request-job-scheduler/index.ts [new file with mode: 0644]
server/lib/jobs/index.ts
server/lib/jobs/job-scheduler.ts
server/lib/jobs/transcoding-job-scheduler/index.ts [new file with mode: 0644]
server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts [new file with mode: 0644]
server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts [new file with mode: 0644]
server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts [new file with mode: 0644]
server/lib/user.ts
server/lib/video-channel.ts
server/middlewares/activitypub.ts [new file with mode: 0644]
server/middlewares/index.ts
server/middlewares/secure.ts [deleted file]
server/middlewares/validators/account.ts [new file with mode: 0644]
server/middlewares/validators/activitypub/index.ts [new file with mode: 0644]
server/middlewares/validators/activitypub/pods.ts [new file with mode: 0644]
server/middlewares/validators/activitypub/signature.ts [new file with mode: 0644]
server/middlewares/validators/activitypub/videos.ts [new file with mode: 0644]
server/middlewares/validators/index.ts
server/middlewares/validators/remote/index.ts [deleted file]
server/middlewares/validators/remote/pods.ts [deleted file]
server/middlewares/validators/remote/signature.ts [deleted file]
server/middlewares/validators/remote/videos.ts [deleted file]
server/models/account/account-follow-interface.ts [new file with mode: 0644]
server/models/account/account-follow.ts [new file with mode: 0644]
server/models/account/account-interface.ts [new file with mode: 0644]
server/models/account/account-video-rate-interface.ts [new file with mode: 0644]
server/models/account/account-video-rate.ts [new file with mode: 0644]
server/models/account/account.ts [new file with mode: 0644]
server/models/account/index.ts [new file with mode: 0644]
server/models/account/user-interface.ts [new file with mode: 0644]
server/models/account/user.ts [new file with mode: 0644]
server/models/index.ts
server/models/job/job-interface.ts
server/models/job/job.ts
server/models/oauth/oauth-token-interface.ts
server/models/pod/pod-interface.ts
server/models/pod/pod.ts
server/models/user/index.ts [deleted file]
server/models/user/user-interface.ts [deleted file]
server/models/user/user-video-rate-interface.ts [deleted file]
server/models/user/user-video-rate.ts [deleted file]
server/models/user/user.ts [deleted file]
server/models/video/author-interface.ts [deleted file]
server/models/video/author.ts [deleted file]
server/models/video/video-channel-interface.ts
server/models/video/video-channel.ts
server/models/video/video-interface.ts
server/models/video/video.ts
shared/models/activitypub/activity.ts [new file with mode: 0644]
shared/models/activitypub/activitypub-actor.ts [new file with mode: 0644]
shared/models/activitypub/activitypub-collection.ts [new file with mode: 0644]
shared/models/activitypub/activitypub-ordered-collection.ts [new file with mode: 0644]
shared/models/activitypub/activitypub-root.ts [new file with mode: 0644]
shared/models/activitypub/activitypub-signature.ts [new file with mode: 0644]
shared/models/activitypub/index.ts [new file with mode: 0644]
shared/models/activitypub/objects/common-objects.ts [new file with mode: 0644]
shared/models/activitypub/objects/index.ts [new file with mode: 0644]
shared/models/activitypub/objects/video-channel-object.ts [new file with mode: 0644]
shared/models/activitypub/objects/video-torrent-object.ts [new file with mode: 0644]
shared/models/activitypub/webfinger.ts [new file with mode: 0644]
shared/models/index.ts
shared/models/job.model.ts
shared/models/videos/video.model.ts
yarn.lock

index 0d432f39c487fa63b8d7db39c982b6316534b28c..a49b4d800702529c9f356831dda0eb61fe94e857 100644 (file)
     "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",
@@ -84,6 +86,7 @@
     "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",
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
new file mode 100644 (file)
index 0000000..28d08b3
--- /dev/null
@@ -0,0 +1,65 @@
+// 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)
+}
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
new file mode 100644 (file)
index 0000000..79d989c
--- /dev/null
@@ -0,0 +1,72 @@
+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)
+  }
+}
diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts
new file mode 100644 (file)
index 0000000..7a4602b
--- /dev/null
@@ -0,0 +1,15 @@
+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
+}
diff --git a/server/controllers/activitypub/pods.ts b/server/controllers/activitypub/pods.ts
new file mode 100644 (file)
index 0000000..326eb61
--- /dev/null
@@ -0,0 +1,69 @@
+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()
+}
diff --git a/server/controllers/activitypub/videos.ts b/server/controllers/activitypub/videos.ts
new file mode 100644 (file)
index 0000000..cba47f0
--- /dev/null
@@ -0,0 +1,589 @@
+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
+  }
+}
diff --git a/server/controllers/api/remote/index.ts b/server/controllers/api/remote/index.ts
deleted file mode 100644 (file)
index d352277..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-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
-}
diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/api/remote/pods.ts
deleted file mode 100644 (file)
index 326eb61..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-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()
-}
diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts
deleted file mode 100644 (file)
index cba47f0..0000000
+++ /dev/null
@@ -1,589 +0,0 @@
-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
-  }
-}
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
new file mode 100644 (file)
index 0000000..ecb509b
--- /dev/null
@@ -0,0 +1,123 @@
+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
+}
index 3dae781447d300a2d665f252ae467d3e399aa629..d8748e1d7e2c7c52060beaa73ae56639f653b383 100644 (file)
@@ -19,8 +19,10 @@ import * as mkdirp from 'mkdirp'
 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'
@@ -54,6 +56,12 @@ function escapeHTML (stringParam) {
   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) => {
@@ -104,13 +112,16 @@ const readdirPromise = promisify1<string, string[]>(readdir)
 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)
 
 // ---------------------------------------------------------------------------
 
@@ -118,9 +129,11 @@ export {
   isTestInstance,
   root,
   escapeHTML,
+  pageToStartAndCount,
 
   promisify0,
   promisify1,
+
   readdirPromise,
   readFilePromise,
   readFileBufferPromise,
@@ -130,11 +143,14 @@ export {
   mkdirpPromise,
   pseudoRandomBytesPromise,
   accessPromise,
-  opensslExecPromise,
+  createPrivateKey,
+  getPublicKey,
   bcryptComparePromise,
   bcryptGenSaltPromise,
   bcryptHashPromise,
   createTorrentPromise,
   rimrafPromise,
-  statPromise
+  statPromise,
+  jsonldSignPromise,
+  jsonldVerifyPromise
 }
diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts
new file mode 100644 (file)
index 0000000..8a7d1b7
--- /dev/null
@@ -0,0 +1,123 @@
+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
+}
diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts
new file mode 100644 (file)
index 0000000..800f0dd
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './account'
+export * from './signature'
+export * from './misc'
+export * from './videos'
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
new file mode 100644 (file)
index 0000000..806d334
--- /dev/null
@@ -0,0 +1,17 @@
+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
+}
diff --git a/server/helpers/custom-validators/activitypub/signature.ts b/server/helpers/custom-validators/activitypub/signature.ts
new file mode 100644 (file)
index 0000000..683ed2b
--- /dev/null
@@ -0,0 +1,22 @@
+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
+}
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
new file mode 100644 (file)
index 0000000..e0ffba6
--- /dev/null
@@ -0,0 +1,184 @@
+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)
+}
index c79982660858095181cd6a940bda28d83f4f1668..869b0887047fcaa041e50fa17a99871d1bc5731a 100644 (file)
@@ -1,4 +1,4 @@
-export * from './remote'
+export * from './activitypub'
 export * from './misc'
 export * from './pods'
 export * from './pods'
diff --git a/server/helpers/custom-validators/remote/index.ts b/server/helpers/custom-validators/remote/index.ts
deleted file mode 100644 (file)
index e29a9b7..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export * from './videos'
diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/remote/videos.ts
deleted file mode 100644 (file)
index e0ffba6..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-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)
-}
index f18b6bd9a12827918ad3626fe17480887481e1aa..c07dddefe9448ece2e4f9469b898a38df0daab58 100644 (file)
@@ -1,4 +1,3 @@
-import * as Promise from 'bluebird'
 import * as ffmpeg from 'fluent-ffmpeg'
 
 import { CONFIG } from '../initializers'
index 846bd796f5ee9172e434ead2a09bc6156a101f8c..2c7ac395486e1e7c378f4b18fdab0bb0b6f63cc3 100644 (file)
@@ -1,3 +1,4 @@
+export * from './activitypub'
 export * from './core-utils'
 export * from './logger'
 export * from './custom-validators'
@@ -6,3 +7,4 @@ export * from './database-utils'
 export * from './peertube-crypto'
 export * from './requests'
 export * from './utils'
+export * from './webfinger'
index 10a226af47b2357d2ec8e133d88fe151389a5b9c..6d50e446f59a124488f2eb392e3212142d426e69 100644 (file)
@@ -1,77 +1,68 @@
-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) {
@@ -80,69 +71,12 @@ 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
 }
index af1f401def6d590bad8b1a21b7a933389852c8bc..8c4c983f7ac0f61ddb4e9ea207f0f049e890e1a6 100644 (file)
@@ -9,7 +9,13 @@ import {
 } 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,
@@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) {
 }
 
 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
 }
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
new file mode 100644 (file)
index 0000000..9586fa5
--- /dev/null
@@ -0,0 +1,44 @@
+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
+    })
+  })
+}
index 9eaef16952978424094381a0f8442caf8648d25d..b69188f7e82b221bf21f6fe2632454072cb787e3 100644 (file)
@@ -2,7 +2,7 @@ import * as config from 'config'
 
 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 () {
index d349abaf0f9f094d1c5ba477479f0423d51183cd..cb838cf16d1035b1c025f92b42a12dc30e7b585a 100644 (file)
@@ -10,7 +10,8 @@ import {
   RequestVideoEventType,
   RequestVideoQaduType,
   RemoteVideoRequestType,
-  JobState
+  JobState,
+  JobCategory
 } from '../../shared/models'
 import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
 
@@ -60,7 +61,6 @@ const CONFIG = {
     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')),
@@ -211,6 +211,10 @@ const FRIEND_SCORE = {
   MAX: 1000
 }
 
+const ACTIVITY_PUB = {
+  COLLECTION_ITEMS_PER_PAGE: 10
+}
+
 // ---------------------------------------------------------------------------
 
 // Number of points we add/remove from a friend after a successful/bad request
@@ -288,17 +292,23 @@ const JOB_STATES: { [ id: string ]: JobState } = {
   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
@@ -368,14 +378,13 @@ export {
   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,
@@ -393,11 +402,11 @@ export {
   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,
index 141566c3ae480d9a7881044f48bd31048bbea6e3..52e7663949791ae80e47b5325f4ef72466db0e6e 100644 (file)
@@ -15,8 +15,9 @@ import { BlacklistedVideoModel } from './../models/video/video-blacklist-interfa
 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'
@@ -26,7 +27,7 @@ import { PodModel } from './../models/pod/pod-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
@@ -38,7 +39,7 @@ const database: {
   init?: (silent: boolean) => Promise<void>,
 
   Application?: ApplicationModel,
-  Author?: AuthorModel,
+  Account?: AccountModel,
   Job?: JobModel,
   OAuthClient?: OAuthClientModel,
   OAuthToken?: OAuthTokenModel,
@@ -48,7 +49,8 @@ const database: {
   RequestVideoQadu?: RequestVideoQaduModel,
   Request?: RequestModel,
   Tag?: TagModel,
-  UserVideoRate?: UserVideoRateModel,
+  AccountVideoRate?: AccountVideoRateModel,
+  AccountFollow?: AccountFollowModel,
   User?: UserModel,
   VideoAbuse?: VideoAbuseModel,
   VideoChannel?: VideoChannelModel,
@@ -126,7 +128,7 @@ async function getModelFiles (modelDirectory: string) {
     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) {
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts
new file mode 100644 (file)
index 0000000..7408006
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './process-create'
+export * from './process-flag'
+export * from './process-update'
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
new file mode 100644 (file)
index 0000000..114ff18
--- /dev/null
@@ -0,0 +1,104 @@
+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) {
+
+}
diff --git a/server/lib/activitypub/process-flag.ts b/server/lib/activitypub/process-flag.ts
new file mode 100644 (file)
index 0000000..6fa862e
--- /dev/null
@@ -0,0 +1,17 @@
+import {
+  ActivityCreate,
+  VideoTorrentObject,
+  VideoChannelObject
+} from '../../../shared'
+
+function processFlagActivity (activity: ActivityCreate) {
+  // empty
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  processFlagActivity
+}
+
+// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts
new file mode 100644 (file)
index 0000000..187c7be
--- /dev/null
@@ -0,0 +1,29 @@
+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) {
+
+}
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
new file mode 100644 (file)
index 0000000..6a31c22
--- /dev/null
@@ -0,0 +1,129 @@
+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)
+}
index d1534b085555e609c5cfac096eebe8cae23d3b97..bfb415ad21731470e29026794e1ddba18257fc2b 100644 (file)
@@ -1,3 +1,4 @@
+export * from './activitypub'
 export * from './cache'
 export * from './jobs'
 export * from './request'
diff --git a/server/lib/jobs/handlers/index.ts b/server/lib/jobs/handlers/index.ts
deleted file mode 100644 (file)
index cef1f89..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-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
-}
diff --git a/server/lib/jobs/handlers/video-file-optimizer.ts b/server/lib/jobs/handlers/video-file-optimizer.ts
deleted file mode 100644 (file)
index ccded47..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-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
-}
diff --git a/server/lib/jobs/handlers/video-file-transcoder.ts b/server/lib/jobs/handlers/video-file-transcoder.ts
deleted file mode 100644 (file)
index 8536455..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-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
-}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
new file mode 100644 (file)
index 0000000..6b6946d
--- /dev/null
@@ -0,0 +1,25 @@
+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
+}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts
new file mode 100644 (file)
index 0000000..42cb913
--- /dev/null
@@ -0,0 +1,17 @@
+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
+}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
new file mode 100644 (file)
index 0000000..6b6946d
--- /dev/null
@@ -0,0 +1,25 @@
+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
+}
diff --git a/server/lib/jobs/http-request-job-scheduler/index.ts b/server/lib/jobs/http-request-job-scheduler/index.ts
new file mode 100644 (file)
index 0000000..4d25732
--- /dev/null
@@ -0,0 +1 @@
+export * from './http-request-job-scheduler'
index b18a3d8459176bf13b87b885309e12c6dc0c4938..a9274370775964ca46ad562b12cf7ccbfee0f20c 100644 (file)
@@ -1 +1,2 @@
-export * from './job-scheduler'
+export * from './http-request-job-scheduler'
+export * from './transcoding-job-scheduler'
index 61d4832681bcbcd6e3caf4edc825ad31bdee6c2e..89a4bca8821f4fc1767512f3279821729f3f9ebf 100644 (file)
@@ -1,39 +1,41 @@
 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) {
@@ -49,7 +51,7 @@ class JobScheduler {
 
         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) {
@@ -64,9 +66,10 @@ class JobScheduler {
     )
   }
 
-  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
     }
@@ -80,7 +83,7 @@ class JobScheduler {
   }
 
   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)
diff --git a/server/lib/jobs/transcoding-job-scheduler/index.ts b/server/lib/jobs/transcoding-job-scheduler/index.ts
new file mode 100644 (file)
index 0000000..73152a1
--- /dev/null
@@ -0,0 +1 @@
+export * from './transcoding-job-scheduler'
diff --git a/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts b/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts
new file mode 100644 (file)
index 0000000..d7c614f
--- /dev/null
@@ -0,0 +1,17 @@
+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
+}
diff --git a/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts
new file mode 100644 (file)
index 0000000..ccded47
--- /dev/null
@@ -0,0 +1,85 @@
+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
+}
diff --git a/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts
new file mode 100644 (file)
index 0000000..8536455
--- /dev/null
@@ -0,0 +1,49 @@
+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
+}
index a92f4777bbdfa74ab4e56165f6d3827de8b3ce3a..57c653e5570010262ccf9d56371c8511729a2a9b 100644 (file)
@@ -1,9 +1,9 @@
 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,
@@ -11,25 +11,25 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
     }
 
     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
@@ -38,5 +38,5 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
 // ---------------------------------------------------------------------------
 
 export {
-  createUserAuthorAndChannel
+  createUserAccountAndChannel
 }
index 678ffe643aff6d48e8b8b660b6ebffdf4c0b3060..a6dd4d061ba3c1a571e571b9a288e32a9de36a03 100644 (file)
@@ -3,15 +3,15 @@ import * as Sequelize from 'sequelize'
 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)
@@ -19,8 +19,8 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author:
 
   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()
 
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
new file mode 100644 (file)
index 0000000..6cf8eea
--- /dev/null
@@ -0,0 +1,57 @@
+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
+}
index cec3e0b2ac91dcd492a866b01c5d76c2cf1aae7e..40480450b45f73e05281934d6b3d61d9644d8654 100644 (file)
@@ -1,9 +1,9 @@
 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'
diff --git a/server/middlewares/secure.ts b/server/middlewares/secure.ts
deleted file mode 100644 (file)
index 5dd809f..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-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
-}
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
new file mode 100644 (file)
index 0000000..5abe942
--- /dev/null
@@ -0,0 +1,53 @@
+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)
+    })
+}
diff --git a/server/middlewares/validators/activitypub/index.ts b/server/middlewares/validators/activitypub/index.ts
new file mode 100644 (file)
index 0000000..f1f2604
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './pods'
+export * from './signature'
+export * from './videos'
diff --git a/server/middlewares/validators/activitypub/pods.ts b/server/middlewares/validators/activitypub/pods.ts
new file mode 100644 (file)
index 0000000..f917b61
--- /dev/null
@@ -0,0 +1,38 @@
+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
+}
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
new file mode 100644 (file)
index 0000000..0ce15c1
--- /dev/null
@@ -0,0 +1,30 @@
+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
+}
diff --git a/server/middlewares/validators/activitypub/videos.ts b/server/middlewares/validators/activitypub/videos.ts
new file mode 100644 (file)
index 0000000..497320c
--- /dev/null
@@ -0,0 +1,61 @@
+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
+}
index 247f6039e9a88f1b56f2313031eeacc2a5f1fdda..46c00d679e998ebdc9c5a95aaf4bb6ace1baa0a7 100644 (file)
@@ -1,5 +1,6 @@
+export * from './account'
 export * from './oembed'
-export * from './remote'
+export * from './activitypub'
 export * from './pagination'
 export * from './pods'
 export * from './sort'
diff --git a/server/middlewares/validators/remote/index.ts b/server/middlewares/validators/remote/index.ts
deleted file mode 100644 (file)
index f1f2604..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './pods'
-export * from './signature'
-export * from './videos'
diff --git a/server/middlewares/validators/remote/pods.ts b/server/middlewares/validators/remote/pods.ts
deleted file mode 100644 (file)
index f917b61..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-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
-}
diff --git a/server/middlewares/validators/remote/signature.ts b/server/middlewares/validators/remote/signature.ts
deleted file mode 100644 (file)
index d3937b5..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-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
-}
diff --git a/server/middlewares/validators/remote/videos.ts b/server/middlewares/validators/remote/videos.ts
deleted file mode 100644 (file)
index 497320c..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-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
-}
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
new file mode 100644 (file)
index 0000000..3be3836
--- /dev/null
@@ -0,0 +1,23 @@
+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> {}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
new file mode 100644 (file)
index 0000000..9bf03b2
--- /dev/null
@@ -0,0 +1,56 @@
+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'
+  })
+}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
new file mode 100644 (file)
index 0000000..2ef3e22
--- /dev/null
@@ -0,0 +1,74 @@
+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> {}
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts
new file mode 100644 (file)
index 0000000..82cbe38
--- /dev/null
@@ -0,0 +1,26 @@
+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> {}
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
new file mode 100644 (file)
index 0000000..7f7c976
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+  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)
+}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
new file mode 100644 (file)
index 0000000..00c0aef
--- /dev/null
@@ -0,0 +1,444 @@
+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)
+  }
+}
diff --git a/server/models/account/index.ts b/server/models/account/index.ts
new file mode 100644 (file)
index 0000000..179f669
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './account-interface'
+export * from './account-follow-interface'
+export * from './account-video-rate-interface'
+export * from './user-interface'
diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts
new file mode 100644 (file)
index 0000000..1a04fb7
--- /dev/null
@@ -0,0 +1,69 @@
+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> {}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
new file mode 100644 (file)
index 0000000..1401762
--- /dev/null
@@ -0,0 +1,311 @@
+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)
+  })
+}
index b392a8a7761033a7ab50dfc752fabf5311e70e75..29479e06756a22b9b05f41130bc0b6ba6ae6a896 100644 (file)
@@ -3,5 +3,5 @@ export * from './job'
 export * from './oauth'
 export * from './pod'
 export * from './request'
-export * from './user'
+export * from './account'
 export * from './video'
index ba5622977e0b317c01e65734dc31f96893891c27..163930a4f93335f7b006470cdbf33adf84c2b8ce 100644 (file)
@@ -1,14 +1,14 @@
 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 {
index 968f9d71ddcba2b85bcc818d8d9bc5befcca5cb2..ce1203e5a0eb291f27a7c4e351a8fea231b164a1 100644 (file)
@@ -1,7 +1,7 @@
 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 {
@@ -13,7 +13,7 @@ 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',
@@ -22,6 +22,10 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
         type: DataTypes.ENUM(values(JOB_STATES)),
         allowNull: false
       },
+      category: {
+        type: DataTypes.ENUM(values(JOB_CATEGORIES)),
+        allowNull: false
+      },
       handlerName: {
         type: DataTypes.STRING,
         allowNull: false
@@ -40,7 +44,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
     }
   )
 
-  const classMethods = [ listWithLimit ]
+  const classMethods = [ listWithLimitByCategory ]
   addMethodsToModel(Job, classMethods)
 
   return Job
@@ -48,7 +52,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
 
 // ---------------------------------------------------------------------------
 
-listWithLimit = function (limit: number, state: JobState) {
+listWithLimitByCategory = function (limit: number, state: JobState) {
   const query = {
     order: [
       [ 'id', 'ASC' ]
index 0c947bde874f50f42b0f36a7071446afc347998c..ef97893c40abbf483908b805fbe63e3253646ac3 100644 (file)
@@ -1,7 +1,7 @@
 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
index 7e095d424f61443c6e50548c68d2cf1e31a8b8e3..6c5aab3fa82db1971e08360d450122813f35f14b 100644 (file)
@@ -48,9 +48,7 @@ export interface PodClass {
 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> {
index 6b33336b8c0f359833290223327474df4015375e..7c8b49bf808e7785680eaf1e4ddd52eb97ab8f2e 100644 (file)
@@ -39,10 +39,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
           }
         }
       },
-      publicKey: {
-        type: DataTypes.STRING(5000),
-        allowNull: false
-      },
       score: {
         type: DataTypes.INTEGER,
         defaultValue: FRIEND_SCORE.BASE,
@@ -51,13 +47,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
           isInt: true,
           max: FRIEND_SCORE.MAX
         }
-      },
-      email: {
-        type: DataTypes.STRING(400),
-        allowNull: false,
-        validate: {
-          isEmail: true
-        }
       }
     },
     {
@@ -100,7 +89,6 @@ toFormattedJSON = function (this: PodInstance) {
   const json = {
     id: this.id,
     host: this.host,
-    email: this.email,
     score: this.score as number,
     createdAt: this.createdAt
   }
diff --git a/server/models/user/index.ts b/server/models/user/index.ts
deleted file mode 100644 (file)
index ed36895..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './user-video-rate-interface'
-export * from './user-interface'
diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts
deleted file mode 100644 (file)
index 49c75aa..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-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> {}
diff --git a/server/models/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts
deleted file mode 100644 (file)
index ea0fdc4..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-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> {}
diff --git a/server/models/user/user-video-rate.ts b/server/models/user/user-video-rate.ts
deleted file mode 100644 (file)
index 7d6dd72..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-  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)
-}
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
deleted file mode 100644 (file)
index b974418..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-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)
-  })
-}
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts
deleted file mode 100644 (file)
index fc69ff3..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-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> {}
diff --git a/server/models/video/author.ts b/server/models/video/author.ts
deleted file mode 100644 (file)
index 43f84c3..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-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)
-}
index b8d3e0f42e4a3cc7e3c8ef20b5ca509d06cac8f6..477f97cd4ea7f045ccf2141da01e066466e617a5 100644 (file)
@@ -1,42 +1,42 @@
 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 {
@@ -45,8 +45,9 @@ export interface VideoChannelAttributes {
   name: string
   description: string
   remote: boolean
+  url: string
 
-  Author?: AuthorInstance
+  Account?: AccountInstance
   Videos?: VideoInstance[]
 }
 
@@ -57,8 +58,7 @@ export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAtt
 
   isOwned: VideoChannelMethods.IsOwned
   toFormattedJSON: VideoChannelMethods.ToFormattedJSON
-  toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
-  toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
+  toActivityPubObject: VideoChannelMethods.ToActivityPubObject
 }
 
 export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
index 46c2db63fa56399e0f7ed7c3992f0876253d8fdc..c17828f3e84963e2d38a3ed6cf6004e8cf106097 100644 (file)
@@ -13,19 +13,18 @@ import {
 
 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',
@@ -62,12 +61,19 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
         type: DataTypes.BOOLEAN,
         allowNull: false,
         defaultValue: false
+      },
+      url: {
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          isUrl: true
+        }
       }
     },
     {
       indexes: [
         {
-          fields: [ 'authorId' ]
+          fields: [ 'accountId' ]
         }
       ],
       hooks: {
@@ -80,21 +86,20 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     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)
 
@@ -118,10 +123,10 @@ toFormattedJSON = function (this: VideoChannelInstance) {
     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
     }
   }
 
@@ -132,27 +137,14 @@ toFormattedJSON = function (this: VideoChannelInstance) {
   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
@@ -161,9 +153,9 @@ toUpdateRemoteJSON = function (this: VideoChannelInstance) {
 // ------------------------------ STATICS ------------------------------
 
 function associate (models) {
-  VideoChannel.belongsTo(models.Author, {
+  VideoChannel.belongsTo(models.Account, {
     foreignKey: {
-      name: 'authorId',
+      name: 'accountId',
       allowNull: false
     },
     onDelete: 'CASCADE'
@@ -190,10 +182,10 @@ function afterDestroy (videoChannel: VideoChannelInstance) {
   return undefined
 }
 
-countByAuthor = function (authorId: number) {
+countByAccount = function (accountId: number) {
   const query = {
     where: {
-      authorId
+      accountId
     }
   }
 
@@ -205,7 +197,7 @@ listOwned = function () {
     where: {
       remote: false
     },
-    include: [ VideoChannel['sequelize'].models.Author ]
+    include: [ VideoChannel['sequelize'].models.Account ]
   }
 
   return VideoChannel.findAll(query)
@@ -218,7 +210,7 @@ listForApi = function (start: number, count: number, sort: string) {
     order: [ getSort(sort) ],
     include: [
       {
-        model: VideoChannel['sequelize'].models.Author,
+        model: VideoChannel['sequelize'].models.Account,
         required: true,
         include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
       }
@@ -230,14 +222,14 @@ listForApi = function (start: number, count: number, sort: string) {
   })
 }
 
-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 } ]
@@ -269,7 +261,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
     },
     include: [
       {
-        model: VideoChannel['sequelize'].models.Author,
+        model: VideoChannel['sequelize'].models.Account,
         include: [
           {
             model: VideoChannel['sequelize'].models.Pod,
@@ -288,15 +280,15 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
   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 } ]
       }
     ]
@@ -305,11 +297,11 @@ loadByIdAndAuthor = function (id: number, authorId: number) {
   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 } ]
       }
     ]
@@ -318,14 +310,14 @@ loadAndPopulateAuthor = function (id: number) {
   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 } ]
       }
     ]
@@ -334,11 +326,11 @@ loadByUUIDAndPopulateAuthor = function (uuid: string) {
   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
index cfe65f9aa7343896633476ea5e55c09a50bac3a2..e62e25a827da314fdc38a67816714775e03b9915 100644 (file)
@@ -1,5 +1,5 @@
 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'
@@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/
 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
@@ -29,8 +30,7 @@ export namespace VideoMethods {
   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>
@@ -40,31 +40,35 @@ export namespace VideoMethods {
   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>
@@ -77,16 +81,17 @@ export interface VideoClass {
   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 {
@@ -104,7 +109,9 @@ export interface VideoAttributes {
   likes?: number
   dislikes?: number
   remote: boolean
+  url: string
 
+  parentId?: number
   channelId?: number
 
   VideoChannel?: VideoChannelInstance
@@ -132,16 +139,18 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
   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>
@@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
 }
 
 export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
+
index 02dde1726ebf433c36d1652dcdca605b275330a4..94af1ece5e2ab0ab6c6950abd3e997b9e954c282 100644 (file)
@@ -5,7 +5,6 @@ import { map, maxBy, truncate } from 'lodash'
 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 {
@@ -52,6 +51,7 @@ 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
@@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName
 let isOwned: VideoMethods.IsOwned
 let toFormattedJSON: VideoMethods.ToFormattedJSON
 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
-let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
-let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
+let toActivityPubObject: VideoMethods.ToActivityPubObject
 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
 let createPreview: VideoMethods.CreatePreview
@@ -76,21 +75,25 @@ let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
 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
@@ -219,6 +222,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
         type: DataTypes.BOOLEAN,
         allowNull: false,
         defaultValue: false
+      },
+      url: {
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          isUrl: true
+        }
       }
     },
     {
@@ -243,6 +253,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
         },
         {
           fields: [ 'channelId' ]
+        },
+        {
+          fields: [ 'parentId' ]
         }
       ],
       hooks: {
@@ -258,16 +271,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     list,
     listForApi,
     listUserVideosForApi,
-    listOwnedAndPopulateAuthorAndTags,
-    listOwnedByAuthor,
+    listOwnedAndPopulateAccountAndTags,
+    listOwnedByAccount,
     load,
-    loadAndPopulateAuthor,
-    loadAndPopulateAuthorAndPodAndTags,
+    loadAndPopulateAccount,
+    loadAndPopulateAccountAndPodAndTags,
     loadByHostAndUUID,
     loadByUUID,
     loadLocalVideoByUUID,
-    loadByUUIDAndPopulateAuthorAndPodAndTags,
-    searchAndPopulateAuthorAndPodAndTags
+    loadByUUIDAndPopulateAccountAndPodAndTags,
+    searchAndPopulateAccountAndPodAndTags
   ]
   const instanceMethods = [
     createPreview,
@@ -286,16 +299,18 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     removePreview,
     removeThumbnail,
     removeTorrent,
-    toAddRemoteJSON,
+    toActivityPubObject,
     toFormattedJSON,
     toFormattedDetailsJSON,
-    toUpdateRemoteJSON,
     optimizeOriginalVideofile,
     transcodeOriginalVideofile,
     getOriginalFileHeight,
     getEmbedPath,
     getTruncatedDescription,
-    getDescriptionPath
+    getDescriptionPath,
+    getCategoryLabel,
+    getLicenceLabel,
+    getLanguageLabel
   ]
   addMethodsToModel(Video, classMethods, instanceMethods)
 
@@ -313,6 +328,14 @@ function associate (models) {
     onDelete: 'cascade'
   })
 
+  Video.belongsTo(models.VideoChannel, {
+    foreignKey: {
+      name: 'parentId',
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+
   Video.belongsToMany(models.Tag, {
     foreignKey: 'videoId',
     through: models.VideoTag,
@@ -423,7 +446,7 @@ getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance)
   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' ]
@@ -433,18 +456,15 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil
     ]
   }
 
-  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) {
@@ -462,40 +482,28 @@ getPreviewPath = 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,
@@ -552,75 +560,75 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
   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) {
@@ -631,7 +639,7 @@ 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()
@@ -643,40 +651,32 @@ optimizeOriginalVideofile = function (this: VideoInstance) {
     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'
 
@@ -696,25 +696,18 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes
     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) {
@@ -727,6 +720,31 @@ getDescriptionPath = 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)
@@ -779,7 +797,7 @@ listUserVideosForApi = function (userId: number, start: number, count: number, s
         required: true,
         include: [
           {
-            model: Video['sequelize'].models.Author,
+            model: Video['sequelize'].models.Account,
             where: {
               userId
             },
@@ -810,7 +828,7 @@ listForApi = function (start: number, count: number, sort: string) {
         model: Video['sequelize'].models.VideoChannel,
         include: [
           {
-            model: Video['sequelize'].models.Author,
+            model: Video['sequelize'].models.Account,
             include: [
               {
                 model: Video['sequelize'].models.Pod,
@@ -846,7 +864,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
         model: Video['sequelize'].models.VideoChannel,
         include: [
           {
-            model: Video['sequelize'].models.Author,
+            model: Video['sequelize'].models.Account,
             include: [
               {
                 model: Video['sequelize'].models.Pod,
@@ -867,7 +885,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
   return Video.findOne(query)
 }
 
-listOwnedAndPopulateAuthorAndTags = function () {
+listOwnedAndPopulateAccountAndTags = function () {
   const query = {
     where: {
       remote: false
@@ -876,7 +894,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
       Video['sequelize'].models.VideoFile,
       {
         model: Video['sequelize'].models.VideoChannel,
-        include: [ Video['sequelize'].models.Author ]
+        include: [ Video['sequelize'].models.Account ]
       },
       Video['sequelize'].models.Tag
     ]
@@ -885,7 +903,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
   return Video.findAll(query)
 }
 
-listOwnedByAuthor = function (author: string) {
+listOwnedByAccount = function (account: string) {
   const query = {
     where: {
       remote: false
@@ -898,9 +916,9 @@ listOwnedByAuthor = function (author: string) {
         model: Video['sequelize'].models.VideoChannel,
         include: [
           {
-            model: Video['sequelize'].models.Author,
+            model: Video['sequelize'].models.Account,
             where: {
-              name: author
+              name: account
             }
           }
         ]
@@ -942,13 +960,13 @@ loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
   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 ]
       }
     ]
   }
@@ -956,14 +974,14 @@ loadAndPopulateAuthor = function (id: number) {
   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 } ]
           }
         ]
@@ -976,7 +994,7 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
   return Video.findById(id, options)
 }
 
-loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
+loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
   const options = {
     where: {
       uuid
@@ -986,7 +1004,7 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
         model: Video['sequelize'].models.VideoChannel,
         include: [
           {
-            model: Video['sequelize'].models.Author,
+            model: Video['sequelize'].models.Account,
             include: [ { model: Video['sequelize'].models.Pod, required: false } ]
           }
         ]
@@ -999,20 +1017,20 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
   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
   }
 
@@ -1045,8 +1063,8 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
       }
     }
     podInclude.required = true
-  } else if (field === 'author') {
-    authorInclude.where = {
+  } else if (field === 'account') {
+    accountInclude.where = {
       name: {
         [Sequelize.Op.iLike]: '%' + value + '%'
       }
@@ -1090,13 +1108,17 @@ function getBaseUrls (video: VideoInstance) {
     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)
 }
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
new file mode 100644 (file)
index 0000000..0274416
--- /dev/null
@@ -0,0 +1,34 @@
+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
+}
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts
new file mode 100644 (file)
index 0000000..7748913
--- /dev/null
@@ -0,0 +1,27 @@
+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
+}
diff --git a/shared/models/activitypub/activitypub-collection.ts b/shared/models/activitypub/activitypub-collection.ts
new file mode 100644 (file)
index 0000000..60a6a6b
--- /dev/null
@@ -0,0 +1,9 @@
+import { Activity } from './activity'
+
+export interface ActivityPubCollection {
+  '@context': string[]
+  type: 'Collection' | 'CollectionPage'
+  totalItems: number
+  partOf?: string
+  items: Activity[]
+}
diff --git a/shared/models/activitypub/activitypub-ordered-collection.ts b/shared/models/activitypub/activitypub-ordered-collection.ts
new file mode 100644 (file)
index 0000000..4080fd7
--- /dev/null
@@ -0,0 +1,9 @@
+import { Activity } from './activity'
+
+export interface ActivityPubOrderedCollection {
+  '@context': string[]
+  type: 'OrderedCollection' | 'OrderedCollectionPage'
+  totalItems: number
+  partOf?: string
+  orderedItems: Activity[]
+}
diff --git a/shared/models/activitypub/activitypub-root.ts b/shared/models/activitypub/activitypub-root.ts
new file mode 100644 (file)
index 0000000..6a67f31
--- /dev/null
@@ -0,0 +1,5 @@
+import { Activity } from './activity'
+import { ActivityPubCollection } from './activitypub-collection'
+import { ActivityPubOrderedCollection } from './activitypub-ordered-collection'
+
+export type RootActivity = Activity | ActivityPubCollection | ActivityPubOrderedCollection
diff --git a/shared/models/activitypub/activitypub-signature.ts b/shared/models/activitypub/activitypub-signature.ts
new file mode 100644 (file)
index 0000000..1d9f4b3
--- /dev/null
@@ -0,0 +1,6 @@
+export interface ActivityPubSignature {
+  type: 'GraphSignature2012'
+  created: Date,
+  creator: string
+  signatureValue: string
+}
diff --git a/shared/models/activitypub/index.ts b/shared/models/activitypub/index.ts
new file mode 100644 (file)
index 0000000..6cacb24
--- /dev/null
@@ -0,0 +1,8 @@
+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'
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts
new file mode 100644 (file)
index 0000000..3eaab21
--- /dev/null
@@ -0,0 +1,25 @@
+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
+}
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts
new file mode 100644 (file)
index 0000000..8c2e2da
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './common-objects'
+export * from './video-channel-object'
+export * from './video-torrent-object'
diff --git a/shared/models/activitypub/objects/video-channel-object.ts b/shared/models/activitypub/objects/video-channel-object.ts
new file mode 100644 (file)
index 0000000..d64b4ae
--- /dev/null
@@ -0,0 +1,8 @@
+import { ActivityIdentifierObject } from './common-objects'
+
+export interface VideoChannelObject {
+  type: 'VideoChannel'
+  name: string
+  content: string
+  uuid: ActivityIdentifierObject
+}
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
new file mode 100644 (file)
index 0000000..00cc0a6
--- /dev/null
@@ -0,0 +1,25 @@
+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[]
+}
diff --git a/shared/models/activitypub/webfinger.ts b/shared/models/activitypub/webfinger.ts
new file mode 100644 (file)
index 0000000..b94baf9
--- /dev/null
@@ -0,0 +1,9 @@
+export interface WebFingerData {
+  subject: string
+  aliases: string[]
+  links: {
+    rel: 'self'
+    type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+    href: string
+  }[]
+}
index 02665a3e6cb8039b80e0f12c549006bc92779736..0ccb84d24628fb880eb31f24948289838ea4afc1 100644 (file)
@@ -1,3 +1,4 @@
+export * from './activitypub'
 export * from './pods'
 export * from './users'
 export * from './videos'
index 411c91482b8f09236cdf7a4f191b5d4ca4ac669a..ab723084a7e66c85079092f9eb195f230326a266 100644 (file)
@@ -1 +1,2 @@
 export type JobState = 'pending' | 'processing' | 'error' | 'success'
+export type JobCategory = 'transcoding' | 'http-request'
index 2f4ee246273d61283b98d9afd4fbcf46804f2031..0606f1aec53cd17099b2463416a8a735b26cd721 100644 (file)
@@ -13,7 +13,7 @@ export interface VideoFile {
 export interface Video {
   id: number
   uuid: string
-  author: string
+  account: string
   createdAt: Date | string
   updatedAt: Date | string
   categoryLabel: string
index eb5d1e13f4f677f2a5ac39ba4eb1d049d34a6c65..52685a8ccd27c2f9612483d51d3800e97cc5c478 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@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"
@@ -456,6 +460,23 @@ bindings@~1.3.0:
   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"
@@ -558,6 +579,14 @@ bluebird@^3.0.5, bluebird@^3.4.6, bluebird@^3.5.0:
   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"
@@ -622,6 +651,10 @@ braces@^1.8.2:
     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"
@@ -630,10 +663,18 @@ browserify-package-json@^1.0.0:
   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"
@@ -726,6 +767,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
     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"
@@ -833,6 +878,12 @@ commander@2.6.0:
   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"
@@ -958,6 +1009,10 @@ cross-spawn@^5.0.1:
     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"
@@ -1148,6 +1203,15 @@ ee-first@1.1.1:
   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"
@@ -1208,10 +1272,22 @@ es6-map@^0.1.3:
     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"
@@ -1834,6 +1910,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2:
   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"
@@ -1890,6 +1970,13 @@ has@^1.0.1:
   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"
@@ -1990,6 +2077,10 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, i
   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"
@@ -2052,7 +2143,7 @@ is-bluebird@^1.0.2:
   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"
 
@@ -2269,6 +2360,35 @@ jsonify@~0.0.0:
   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"
@@ -2439,6 +2559,10 @@ lodash@4.17.4, lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.16.0, lo
   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"
@@ -2479,6 +2603,14 @@ map-stream@~0.1.0:
   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"
@@ -2539,6 +2671,10 @@ mimic-response@^1.0.0:
   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"
@@ -2667,6 +2803,10 @@ node-abi@^2.1.1:
   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"
@@ -2820,10 +2960,6 @@ onetime@^1.0.0:
   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"
@@ -2839,7 +2975,7 @@ os-homedir@1.0.2, os-homedir@^1.0.0, os-homedir@^1.0.1:
   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"
 
@@ -2970,6 +3106,15 @@ pause-stream@0.0.11:
   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"
@@ -3074,6 +3219,14 @@ pkg-up@^1.0.0:
   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"
@@ -3353,7 +3506,7 @@ request@2.81.0:
     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:
@@ -4255,6 +4408,12 @@ videostream@^2.3.0:
     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"
@@ -4302,7 +4461,7 @@ webtorrent@^0.98.0:
     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:
@@ -4378,6 +4537,14 @@ xdg-basedir@^3.0.0:
   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"