]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
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)
92 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 [moved from server/controllers/api/remote/pods.ts with 100% similarity]
server/controllers/activitypub/videos.ts [moved from server/controllers/api/remote/videos.ts with 100% similarity]
server/controllers/api/remote/index.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 [moved from server/helpers/custom-validators/remote/videos.ts with 100% similarity]
server/helpers/custom-validators/index.ts
server/helpers/custom-validators/remote/index.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/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 [moved from server/lib/jobs/handlers/video-file-optimizer.ts with 100% similarity]
server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts [moved from server/lib/jobs/handlers/video-file-transcoder.ts with 100% similarity]
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 [moved from server/middlewares/validators/remote/index.ts with 100% similarity]
server/middlewares/validators/activitypub/pods.ts [moved from server/middlewares/validators/remote/pods.ts with 100% similarity]
server/middlewares/validators/activitypub/signature.ts [new file with mode: 0644]
server/middlewares/validators/activitypub/videos.ts [moved from server/middlewares/validators/remote/videos.ts with 100% similarity]
server/middlewares/validators/index.ts
server/middlewares/validators/remote/signature.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 [moved from server/models/user/user-video-rate.ts with 51% similarity]
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 [moved from server/models/user/user-interface.ts with 81% similarity]
server/models/account/user.ts [moved from server/models/user/user.ts with 89% similarity]
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-video-rate-interface.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/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/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
+}
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'
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/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
+}
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/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
+}
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/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/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> {}
similarity index 51%
rename from server/models/user/user-video-rate.ts
rename to server/models/account/account-video-rate.ts
index 7d6dd7281d9d1407b5f6ebbda688ffdcb0ed13f4..7f7c976068a39f2c5b64625dbdd714f904bc434e 100644 (file)
@@ -1,5 +1,5 @@
 /*
-  User rates per video.
+  Account rates per video.
 */
 import { values } from 'lodash'
 import * as Sequelize from 'sequelize'
@@ -8,17 +8,17 @@ import { VIDEO_RATE_TYPES } from '../../initializers'
 
 import { addMethodsToModel } from '../utils'
 import {
-  UserVideoRateInstance,
-  UserVideoRateAttributes,
+  AccountVideoRateInstance,
+  AccountVideoRateAttributes,
 
-  UserVideoRateMethods
-} from './user-video-rate-interface'
+  AccountVideoRateMethods
+} from './account-video-rate-interface'
 
-let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes>
-let load: UserVideoRateMethods.Load
+let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
+let load: AccountVideoRateMethods.Load
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
-  UserVideoRate = sequelize.define<UserVideoRateInstance, UserVideoRateAttributes>('UserVideoRate',
+  AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
     {
       type: {
         type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
@@ -28,7 +28,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     {
       indexes: [
         {
-          fields: [ 'videoId', 'userId', 'type' ],
+          fields: [ 'videoId', 'accountId', 'type' ],
           unique: true
         }
       ]
@@ -40,15 +40,15 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 
     load
   ]
-  addMethodsToModel(UserVideoRate, classMethods)
+  addMethodsToModel(AccountVideoRate, classMethods)
 
-  return UserVideoRate
+  return AccountVideoRate
 }
 
 // ------------------------------ STATICS ------------------------------
 
 function associate (models) {
-  UserVideoRate.belongsTo(models.Video, {
+  AccountVideoRate.belongsTo(models.Video, {
     foreignKey: {
       name: 'videoId',
       allowNull: false
@@ -56,23 +56,23 @@ function associate (models) {
     onDelete: 'CASCADE'
   })
 
-  UserVideoRate.belongsTo(models.User, {
+  AccountVideoRate.belongsTo(models.Account, {
     foreignKey: {
-      name: 'userId',
+      name: 'accountId',
       allowNull: false
     },
     onDelete: 'CASCADE'
   })
 }
 
-load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) {
-  const options: Sequelize.FindOptions<UserVideoRateAttributes> = {
+load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) {
+  const options: Sequelize.FindOptions<AccountVideoRateAttributes> = {
     where: {
-      userId,
+      accountId,
       videoId
     }
   }
   if (transaction) options.transaction = transaction
 
-  return UserVideoRate.findOne(options)
+  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'
similarity index 81%
rename from server/models/user/user-interface.ts
rename to server/models/account/user-interface.ts
index 49c75aa3be84be1227aa1f4ab0cc0cf34216edad..1a04fb7505ce980deda11856bf35a3f2b34990c4 100644 (file)
@@ -1,10 +1,10 @@
 import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
+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 { AuthorInstance } from '../video/author-interface'
 import { UserRight } from '../../../shared/models/users/user-right.enum'
 import { UserRole } from '../../../shared/models/users/user-role'
 
@@ -15,18 +15,18 @@ export namespace UserMethods {
   export type ToFormattedJSON = (this: UserInstance) => FormattedUser
   export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
 
-  export type CountTotal = () => Promise<number>
+  export type CountTotal = () => Bluebird<number>
 
-  export type GetByUsername = (username: string) => Promise<UserInstance>
+  export type GetByUsername = (username: string) => Bluebird<UserInstance>
 
-  export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
+  export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
 
-  export type LoadById = (id: number) => Promise<UserInstance>
+  export type LoadById = (id: number) => Bluebird<UserInstance>
 
-  export type LoadByUsername = (username: string) => Promise<UserInstance>
-  export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance>
+  export type LoadByUsername = (username: string) => Bluebird<UserInstance>
+  export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
 
-  export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
+  export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
 }
 
 export interface UserClass {
@@ -53,7 +53,7 @@ export interface UserAttributes {
   role: UserRole
   videoQuota: number
 
-  Author?: AuthorInstance
+  Account?: AccountInstance
 }
 
 export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
similarity index 89%
rename from server/models/user/user.ts
rename to server/models/account/user.ts
index b974418d4ba61a8f96190984cb3cb31599141a5d..1401762c5c4da81023a55c321a599b992940995d 100644 (file)
@@ -1,5 +1,4 @@
 import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
 
 import { getSort, addMethodsToModel } from '../utils'
 import {
@@ -166,13 +165,13 @@ toFormattedJSON = function (this: UserInstance) {
     videoQuota: this.videoQuota,
     createdAt: this.createdAt,
     author: {
-      id: this.Author.id,
-      uuid: this.Author.uuid
+      id: this.Account.id,
+      uuid: this.Account.uuid
     }
   }
 
-  if (Array.isArray(this.Author.VideoChannels) === true) {
-    const videoChannels = this.Author.VideoChannels
+  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
@@ -198,7 +197,7 @@ isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.Fi
 // ------------------------------ STATICS ------------------------------
 
 function associate (models) {
-  User.hasOne(models.Author, {
+  User.hasOne(models.Account, {
     foreignKey: 'userId',
     onDelete: 'cascade'
   })
@@ -218,7 +217,7 @@ getByUsername = function (username: string) {
     where: {
       username: username
     },
-    include: [ { model: User['sequelize'].models.Author, required: true } ]
+    include: [ { model: User['sequelize'].models.Account, required: true } ]
   }
 
   return User.findOne(query)
@@ -229,7 +228,7 @@ listForApi = function (start: number, count: number, sort: string) {
     offset: start,
     limit: count,
     order: [ getSort(sort) ],
-    include: [ { model: User['sequelize'].models.Author, required: true } ]
+    include: [ { model: User['sequelize'].models.Account, required: true } ]
   }
 
   return User.findAndCountAll(query).then(({ rows, count }) => {
@@ -242,7 +241,7 @@ listForApi = function (start: number, count: number, sort: string) {
 
 loadById = function (id: number) {
   const options = {
-    include: [ { model: User['sequelize'].models.Author, required: true } ]
+    include: [ { model: User['sequelize'].models.Account, required: true } ]
   }
 
   return User.findById(id, options)
@@ -253,7 +252,7 @@ loadByUsername = function (username: string) {
     where: {
       username
     },
-    include: [ { model: User['sequelize'].models.Author, required: true } ]
+    include: [ { model: User['sequelize'].models.Account, required: true } ]
   }
 
   return User.findOne(query)
@@ -266,7 +265,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
     },
     include: [
       {
-        model: User['sequelize'].models.Author,
+        model: User['sequelize'].models.Account,
         required: true,
         include: [ User['sequelize'].models.VideoChannel ]
       }
@@ -278,7 +277,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
 
 loadByUsernameOrEmail = function (username: string, email: string) {
   const query = {
-    include: [ { model: User['sequelize'].models.Author, required: true } ],
+    include: [ { model: User['sequelize'].models.Account, required: true } ],
     where: {
       [Sequelize.Op.or]: [ { username }, { email } ]
     }
@@ -296,8 +295,8 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
                 '(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" ' +
+                '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 = {
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-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/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"