From: Chocobozzz Date: Thu, 9 Nov 2017 16:51:58 +0000 (+0100) Subject: Begin activitypub X-Git-Tag: v0.0.1-alpha~229 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=e4f97babf701481b55cc10fb3448feab5f97c867;p=github%2FChocobozzz%2FPeerTube.git Begin activitypub --- diff --git a/package.json b/package.json index 0d432f39c..a49b4d800 100644 --- a/package.json +++ b/package.json @@ -64,14 +64,16 @@ "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" }, @@ -102,6 +105,7 @@ "@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 index 000000000..28d08b3f4 --- /dev/null +++ b/server/controllers/activitypub/client.ts @@ -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 index 000000000..79d989c2c --- /dev/null +++ b/server/controllers/activitypub/inbox.ts @@ -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 } = { + 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 index 000000000..7a4602b37 --- /dev/null +++ b/server/controllers/activitypub/index.ts @@ -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/pods.ts b/server/controllers/activitypub/pods.ts similarity index 100% rename from server/controllers/api/remote/pods.ts rename to server/controllers/activitypub/pods.ts diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/activitypub/videos.ts similarity index 100% rename from server/controllers/api/remote/videos.ts rename to server/controllers/activitypub/videos.ts diff --git a/server/controllers/api/remote/index.ts b/server/controllers/api/remote/index.ts deleted file mode 100644 index d3522772b..000000000 --- a/server/controllers/api/remote/index.ts +++ /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 index 000000000..ecb509b66 --- /dev/null +++ b/server/helpers/activitypub.ts @@ -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) { + 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 +} diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 3dae78144..d8748e1d7 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -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 (func: (cb: (err: any, result: A) => void) => void): () => Promise { return function promisified (): Promise { return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { @@ -104,13 +112,16 @@ const readdirPromise = promisify1(readdir) const mkdirpPromise = promisify1(mkdirp) const pseudoRandomBytesPromise = promisify1(pseudoRandomBytes) const accessPromise = promisify1WithVoid(access) -const opensslExecPromise = promisify2WithVoid(openssl.exec) +const createPrivateKey = promisify1(pem.createPrivateKey) +const getPublicKey = promisify1(pem.getPublicKey) const bcryptComparePromise = promisify2(bcrypt.compare) const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) const bcryptHashPromise = promisify2(bcrypt.hash) const createTorrentPromise = promisify2(createTorrent) const rimrafPromise = promisify1WithVoid(rimraf) const statPromise = promisify1(stat) +const jsonldSignPromise = promisify2(jsig.sign) +const jsonldVerifyPromise = promisify2(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 index 000000000..8a7d1b7fe --- /dev/null +++ b/server/helpers/custom-validators/activitypub/account.ts @@ -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 index 000000000..800f0ddf3 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/index.ts @@ -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 index 000000000..806d33483 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/misc.ts @@ -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 index 000000000..683ed2b1c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/signature.ts @@ -0,0 +1,22 @@ +import { exists } from '../misc' +import { isActivityPubUrlValid } from './misc' + +function isSignatureTypeValid (signatureType: string) { + return exists(signatureType) && signatureType === 'GraphSignature2012' +} + +function isSignatureCreatorValid (signatureCreator: string) { + return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator) +} + +function isSignatureValueValid (signatureValue: string) { + return exists(signatureValue) && signatureValue.length > 0 +} + +// --------------------------------------------------------------------------- + +export { + isSignatureTypeValid, + isSignatureCreatorValid, + isSignatureValueValid +} diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts similarity index 100% rename from server/helpers/custom-validators/remote/videos.ts rename to server/helpers/custom-validators/activitypub/videos.ts diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts index c79982660..869b08870 100644 --- a/server/helpers/custom-validators/index.ts +++ b/server/helpers/custom-validators/index.ts @@ -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 index e29a9b767..000000000 --- a/server/helpers/custom-validators/remote/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './videos' diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f18b6bd9a..c07dddefe 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,4 +1,3 @@ -import * as Promise from 'bluebird' import * as ffmpeg from 'fluent-ffmpeg' import { CONFIG } from '../initializers' diff --git a/server/helpers/index.ts b/server/helpers/index.ts index 846bd796f..2c7ac3954 100644 --- a/server/helpers/index.ts +++ b/server/helpers/index.ts @@ -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' diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 10a226af4..6d50e446f 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -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 } diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index af1f401de..8c4c983f7 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -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 index 000000000..9586fa562 --- /dev/null +++ b/server/helpers/webfinger.ts @@ -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((res, rej) => { + webfinger.lookup('nick@silverbucket.net', (err, p) => { + if (err) return rej(err) + + return p + }) + }) +} diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 9eaef1695..b69188f7e 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts @@ -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 () { diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index d349abaf0..cb838cf16 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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('database.password') }, STORAGE: { - CERT_DIR: join(root(), config.get('storage.certs')), LOG_DIR: join(root(), config.get('storage.logs')), VIDEOS_DIR: join(root(), config.get('storage.videos')), THUMBNAILS_DIR: join(root(), config.get('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, diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 141566c3a..52e766394 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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, 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[] = [] + const tasks: Promise[] = [] // 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 index 000000000..740800606 --- /dev/null +++ b/server/lib/activitypub/index.ts @@ -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 index 000000000..114ff1848 --- /dev/null +++ b/server/lib/activitypub/process-create.ts @@ -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 index 000000000..6fa862ee9 --- /dev/null +++ b/server/lib/activitypub/process-flag.ts @@ -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 index 000000000..187c7be7c --- /dev/null +++ b/server/lib/activitypub/process-update.ts @@ -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 index 000000000..6a31c226d --- /dev/null +++ b/server/lib/activitypub/send-request.ts @@ -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 +} + +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) +} diff --git a/server/lib/index.ts b/server/lib/index.ts index d1534b085..bfb415ad2 100644 --- a/server/lib/index.ts +++ b/server/lib/index.ts @@ -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 index cef1f89a9..000000000 --- a/server/lib/jobs/handlers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as videoFileOptimizer from './video-file-optimizer' -import * as videoFileTranscoder from './video-file-transcoder' - -export interface JobHandler { - process (data: object, jobId: number): T - onError (err: Error, jobId: number) - onSuccess (jobId: number, jobResult: T) -} - -const jobHandlers: { [ handlerName: string ]: JobHandler } = { - 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 index 000000000..6b6946d02 --- /dev/null +++ b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts @@ -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 index 000000000..42cb9139c --- /dev/null +++ b/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts @@ -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 } = { + 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 index 000000000..6b6946d02 --- /dev/null +++ b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts @@ -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 index 000000000..4d2573296 --- /dev/null +++ b/server/lib/jobs/http-request-job-scheduler/index.ts @@ -0,0 +1 @@ +export * from './http-request-job-scheduler' diff --git a/server/lib/jobs/index.ts b/server/lib/jobs/index.ts index b18a3d845..a92743707 100644 --- a/server/lib/jobs/index.ts +++ b/server/lib/jobs/index.ts @@ -1 +1,2 @@ -export * from './job-scheduler' +export * from './http-request-job-scheduler' +export * from './transcoding-job-scheduler' diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts index 61d483268..89a4bca88 100644 --- a/server/lib/jobs/job-scheduler.ts +++ b/server/lib/jobs/job-scheduler.ts @@ -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 { + 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 { - private constructor () { } - - static get Instance () { - return this.instance || (this.instance = new this()) - } + constructor ( + private jobCategory: JobCategory, + private jobHandlers: { [ id: string ]: JobHandler } + ) {} 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(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 index 000000000..73152a1be --- /dev/null +++ b/server/lib/jobs/transcoding-job-scheduler/index.ts @@ -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 index 000000000..d7c614fb8 --- /dev/null +++ b/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts @@ -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 } = { + videoFileOptimizer, + videoFileTranscoder +} +const jobCategory: JobCategory = 'transcoding' + +const transcodingJobScheduler = new JobScheduler(jobCategory, jobHandlers) + +export { + transcodingJobScheduler +} diff --git a/server/lib/jobs/handlers/video-file-optimizer.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts similarity index 100% rename from server/lib/jobs/handlers/video-file-optimizer.ts rename to server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts diff --git a/server/lib/jobs/handlers/video-file-transcoder.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts similarity index 100% rename from server/lib/jobs/handlers/video-file-transcoder.ts rename to server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts diff --git a/server/lib/user.ts b/server/lib/user.ts index a92f4777b..57c653e55 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -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 } diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 678ffe643..a6dd4d061 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts @@ -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 index 000000000..6cf8eea6f --- /dev/null +++ b/server/middlewares/activitypub.ts @@ -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 +} diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index cec3e0b2a..40480450b 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -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 index 5dd809f15..000000000 --- a/server/middlewares/secure.ts +++ /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 index 000000000..5abe942d6 --- /dev/null +++ b/server/middlewares/validators/account.ts @@ -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/remote/index.ts b/server/middlewares/validators/activitypub/index.ts similarity index 100% rename from server/middlewares/validators/remote/index.ts rename to server/middlewares/validators/activitypub/index.ts diff --git a/server/middlewares/validators/remote/pods.ts b/server/middlewares/validators/activitypub/pods.ts similarity index 100% rename from server/middlewares/validators/remote/pods.ts rename to server/middlewares/validators/activitypub/pods.ts diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts new file mode 100644 index 000000000..0ce15c1f6 --- /dev/null +++ b/server/middlewares/validators/activitypub/signature.ts @@ -0,0 +1,30 @@ +import { body } from 'express-validator/check' +import * as express from 'express' + +import { + logger, + isDateValid, + isSignatureTypeValid, + isSignatureCreatorValid, + isSignatureValueValid +} from '../../../helpers' +import { checkErrors } from '../utils' + +const signatureValidator = [ + body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), + body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), + body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), + body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) + + checkErrors(req, res, next) + } +] + +// --------------------------------------------------------------------------- + +export { + signatureValidator +} diff --git a/server/middlewares/validators/remote/videos.ts b/server/middlewares/validators/activitypub/videos.ts similarity index 100% rename from server/middlewares/validators/remote/videos.ts rename to server/middlewares/validators/activitypub/videos.ts diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 247f6039e..46c00d679 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -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 index d3937b515..000000000 --- a/server/middlewares/validators/remote/signature.ts +++ /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 index 000000000..3be383649 --- /dev/null +++ b/server/models/account/account-follow-interface.ts @@ -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 { + id: number + createdAt: Date + updatedAt: Date +} + +export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model {} diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts new file mode 100644 index 000000000..9bf03b253 --- /dev/null +++ b/server/models/account/account-follow.ts @@ -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 + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + AccountFollow = sequelize.define('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 index 000000000..2ef3e2246 --- /dev/null +++ b/server/models/account/account-interface.ts @@ -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 + export type LoadByUUID = (uuid: string) => Bluebird + export type LoadByUrl = (url: string) => Bluebird + export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird + export type LoadLocalAccountByName = (name: string) => Bluebird + export type ListOwned = () => Bluebird + export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList > + export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList > + + export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor + export type IsOwned = (this: AccountInstance) => boolean + export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird + 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 { + 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 {} diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts new file mode 100644 index 000000000..82cbe38cc --- /dev/null +++ b/server/models/account/account-video-rate-interface.ts @@ -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 +} + +export interface AccountVideoRateClass { + load: AccountVideoRateMethods.Load +} + +export interface AccountVideoRateAttributes { + type: VideoRateType + accountId: number + videoId: number +} + +export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model {} diff --git a/server/models/user/user-video-rate.ts b/server/models/account/account-video-rate.ts similarity index 51% rename from server/models/user/user-video-rate.ts rename to server/models/account/account-video-rate.ts index 7d6dd7281..7f7c97606 100644 --- a/server/models/user/user-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -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 -let load: UserVideoRateMethods.Load +let AccountVideoRate: Sequelize.Model +let load: AccountVideoRateMethods.Load export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - UserVideoRate = sequelize.define('UserVideoRate', + AccountVideoRate = sequelize.define('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 = { +load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { + const options: Sequelize.FindOptions = { 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 index 000000000..00c0aefd4 --- /dev/null +++ b/server/models/account/account.ts @@ -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 +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('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 = { + 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 = { + 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 = { + where: { + uuid + } + } + + return Account.findOne(query) +} + +loadLocalAccountByName = function (name: string) { + const query: Sequelize.FindOptions = { + where: { + name, + userId: { + [Sequelize.Op.ne]: null + } + } + } + + return Account.findOne(query) +} + +loadByUrl = function (url: string) { + const query: Sequelize.FindOptions = { + where: { + url + } + } + + return Account.findOne(query) +} + +loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + 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[] = [] + + 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 index 000000000..179f66974 --- /dev/null +++ b/server/models/account/index.ts @@ -0,0 +1,4 @@ +export * from './account-interface' +export * from './account-follow-interface' +export * from './account-video-rate-interface' +export * from './user-interface' diff --git a/server/models/user/user-interface.ts b/server/models/account/user-interface.ts similarity index 81% rename from server/models/user/user-interface.ts rename to server/models/account/user-interface.ts index 49c75aa3b..1a04fb750 100644 --- a/server/models/user/user-interface.ts +++ b/server/models/account/user-interface.ts @@ -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 - export type CountTotal = () => Promise + export type CountTotal = () => Bluebird - export type GetByUsername = (username: string) => Promise + export type GetByUsername = (username: string) => Bluebird - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > + export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > - export type LoadById = (id: number) => Promise + export type LoadById = (id: number) => Bluebird - export type LoadByUsername = (username: string) => Promise - export type LoadByUsernameAndPopulateChannels = (username: string) => Promise + export type LoadByUsername = (username: string) => Bluebird + export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird - export type LoadByUsernameOrEmail = (username: string, email: string) => Promise + export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird } 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 { diff --git a/server/models/user/user.ts b/server/models/account/user.ts similarity index 89% rename from server/models/user/user.ts rename to server/models/account/user.ts index b974418d4..1401762c5 100644 --- a/server/models/user/user.ts +++ b/server/models/account/user.ts @@ -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 = { diff --git a/server/models/index.ts b/server/models/index.ts index b392a8a77..29479e067 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -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' diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts index ba5622977..163930a4f 100644 --- a/server/models/job/job-interface.ts +++ b/server/models/job/job-interface.ts @@ -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 + export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise } export interface JobClass { - listWithLimit: JobMethods.ListWithLimit + listWithLimitByCategory: JobMethods.ListWithLimitByCategory } export interface JobAttributes { diff --git a/server/models/job/job.ts b/server/models/job/job.ts index 968f9d71d..ce1203e5a 100644 --- a/server/models/job/job.ts +++ b/server/models/job/job.ts @@ -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 -let listWithLimit: JobMethods.ListWithLimit +let listWithLimitByCategory: JobMethods.ListWithLimitByCategory export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { Job = sequelize.define('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' ] diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts index 0c947bde8..ef97893c4 100644 --- a/server/models/oauth/oauth-token-interface.ts +++ b/server/models/oauth/oauth-token-interface.ts @@ -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 diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts index 7e095d424..6c5aab3fa 100644 --- a/server/models/pod/pod-interface.ts +++ b/server/models/pod/pod-interface.ts @@ -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 { diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts index 6b33336b8..7c8b49bf8 100644 --- a/server/models/pod/pod.ts +++ b/server/models/pod/pod.ts @@ -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 index ed3689518..000000000 --- a/server/models/user/index.ts +++ /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 index ea0fdc4d9..000000000 --- a/server/models/user/user-video-rate-interface.ts +++ /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 -} - -export interface UserVideoRateClass { - load: UserVideoRateMethods.Load -} - -export interface UserVideoRateAttributes { - type: VideoRateType - userId: number - videoId: number -} - -export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model {} diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts deleted file mode 100644 index fc69ff3c2..000000000 --- a/server/models/video/author-interface.ts +++ /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 - export type LoadByUUID = (uuid: string) => Promise - export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise - export type ListOwned = () => Promise - - export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData - export type IsOwned = (this: AuthorInstance) => boolean -} - -export interface AuthorClass { - loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID - load: AuthorMethods.Load - loadByUUID: AuthorMethods.LoadByUUID - listOwned: AuthorMethods.ListOwned -} - -export interface AuthorAttributes { - name: string - uuid?: string - - podId?: number - userId?: number -} - -export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance { - isOwned: AuthorMethods.IsOwned - toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON - - id: number - createdAt: Date - updatedAt: Date - - Pod: PodInstance - VideoChannels: VideoChannelInstance[] -} - -export interface AuthorModel extends AuthorClass, Sequelize.Model {} diff --git a/server/models/video/author.ts b/server/models/video/author.ts deleted file mode 100644 index 43f84c3ea..000000000 --- a/server/models/video/author.ts +++ /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 -let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID -let load: AuthorMethods.Load -let loadByUUID: AuthorMethods.LoadByUUID -let listOwned: AuthorMethods.ListOwned -let isOwned: AuthorMethods.IsOwned -let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON - -export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Author = sequelize.define('Author', - { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - 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 = { - where: { - podId: null - } - } - - return Author.findAll(query) -} - -load = function (id: number) { - return Author.findById(id) -} - -loadByUUID = function (uuid: string) { - const query: Sequelize.FindOptions = { - where: { - uuid - } - } - - return Author.findOne(query) -} - -loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - podId, - uuid - }, - transaction - } - - return Author.find(query) -} diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts index b8d3e0f42..477f97cd4 100644 --- a/server/models/video/video-channel-interface.ts +++ b/server/models/video/video-channel-interface.ts @@ -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 + export type CountByAccount = (accountId: number) => Promise export type ListOwned = () => Promise export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise - export type ListByAuthor = (authorId: number) => Promise< ResultList > - export type LoadAndPopulateAuthor = (id: number) => Promise - export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise + export type LoadByIdAndAccount = (id: number, accountId: number) => Promise + export type ListByAccount = (accountId: number) => Promise< ResultList > + export type LoadAndPopulateAccount = (id: number) => Promise + export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise - export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise + export type LoadAndPopulateAccountAndVideos = (id: number) => Promise } 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 {} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 46c2db63f..c17828f3e 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -13,19 +13,18 @@ import { let VideoChannel: Sequelize.Model 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('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 diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index cfe65f9aa..e62e25a82 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -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 - export type ToAddRemoteJSON = (this: VideoInstance) => Promise - export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData + export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise @@ -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 - export type List = () => Promise - export type ListOwnedAndPopulateAuthorAndTags = () => Promise - export type ListOwnedByAuthor = (author: string) => Promise + export type List = () => Bluebird + export type ListOwnedAndPopulateAccountAndTags = () => Bluebird + export type ListOwnedByAccount = (account: string) => Bluebird - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList > - export type SearchAndPopulateAuthorAndPodAndTags = ( + export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > + export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList > + export type SearchAndPopulateAccountAndPodAndTags = ( value: string, field: string, start: number, count: number, sort: string - ) => Promise< ResultList > + ) => Bluebird< ResultList > - export type Load = (id: number) => Promise - export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadAndPopulateAuthor = (id: number) => Promise - export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise - export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise + export type Load = (id: number) => Bluebird + export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird + export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadAndPopulateAccount = (id: number) => Bluebird + export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird + export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird export type RemoveThumbnail = (this: VideoInstance) => Promise export type RemovePreview = (this: VideoInstance) => Promise @@ -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 addVideoFile: Sequelize.HasManyAddAssociationMixin @@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In } export interface VideoModel extends VideoClass, Sequelize.Model {} + diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 02dde1726..94af1ece5 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 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(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(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 index 000000000..0274416b2 --- /dev/null +++ b/shared/models/activitypub/activity.ts @@ -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 index 000000000..77489135c --- /dev/null +++ b/shared/models/activitypub/activitypub-actor.ts @@ -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 index 000000000..60a6a6b04 --- /dev/null +++ b/shared/models/activitypub/activitypub-collection.ts @@ -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 index 000000000..4080fd740 --- /dev/null +++ b/shared/models/activitypub/activitypub-ordered-collection.ts @@ -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 index 000000000..6a67f3101 --- /dev/null +++ b/shared/models/activitypub/activitypub-root.ts @@ -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 index 000000000..1d9f4b3b3 --- /dev/null +++ b/shared/models/activitypub/activitypub-signature.ts @@ -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 index 000000000..6cacb24d2 --- /dev/null +++ b/shared/models/activitypub/index.ts @@ -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 index 000000000..3eaab21b5 --- /dev/null +++ b/shared/models/activitypub/objects/common-objects.ts @@ -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 index 000000000..8c2e2daca --- /dev/null +++ b/shared/models/activitypub/objects/index.ts @@ -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 index 000000000..d64b4aed8 --- /dev/null +++ b/shared/models/activitypub/objects/video-channel-object.ts @@ -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 index 000000000..00cc0a649 --- /dev/null +++ b/shared/models/activitypub/objects/video-torrent-object.ts @@ -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 index 000000000..b94baf996 --- /dev/null +++ b/shared/models/activitypub/webfinger.ts @@ -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 + }[] +} diff --git a/shared/models/index.ts b/shared/models/index.ts index 02665a3e6..0ccb84d24 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -1,3 +1,4 @@ +export * from './activitypub' export * from './pods' export * from './users' export * from './videos' diff --git a/shared/models/job.model.ts b/shared/models/job.model.ts index 411c91482..ab723084a 100644 --- a/shared/models/job.model.ts +++ b/shared/models/job.model.ts @@ -1 +1,2 @@ export type JobState = 'pending' | 'processing' | 'error' | 'success' +export type JobCategory = 'transcoding' | 'http-request' diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 2f4ee2462..0606f1aec 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -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 diff --git a/yarn.lock b/yarn.lock index eb5d1e13f..52685a8cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,6 +125,10 @@ "@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"