Begin activitypub

This commit is contained in:
Chocobozzz 2017-11-09 17:51:58 +01:00
parent 343ad675f2
commit e4f97babf7
No known key found for this signature in database
GPG key ID: 583A612D890159BE
92 changed files with 2507 additions and 920 deletions

View file

@ -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",

View file

@ -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)
}

View file

@ -0,0 +1,72 @@
import * as express from 'express'
import {
processCreateActivity,
processUpdateActivity,
processFlagActivity
} from '../../lib'
import {
Activity,
ActivityType,
RootActivity,
ActivityPubCollection,
ActivityPubOrderedCollection
} from '../../../shared'
import {
signatureValidator,
checkSignature,
asyncMiddleware
} from '../../middlewares'
import { logger } from '../../helpers'
const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
Create: processCreateActivity,
Update: processUpdateActivity,
Flag: processFlagActivity
}
const inboxRouter = express.Router()
inboxRouter.post('/',
signatureValidator,
asyncMiddleware(checkSignature),
// inboxValidator,
asyncMiddleware(inboxController)
)
// ---------------------------------------------------------------------------
export {
inboxRouter
}
// ---------------------------------------------------------------------------
async function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
const rootActivity: RootActivity = req.body
let activities: Activity[] = []
if ([ 'Collection', 'CollectionPage' ].indexOf(rootActivity.type) !== -1) {
activities = (rootActivity as ActivityPubCollection).items
} else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].indexOf(rootActivity.type) !== -1) {
activities = (rootActivity as ActivityPubOrderedCollection).orderedItems
} else {
activities = [ rootActivity as Activity ]
}
await processActivities(activities)
res.status(204).end()
}
async function processActivities (activities: Activity[]) {
for (const activity of activities) {
const activityProcessor = processActivity[activity.type]
if (activityProcessor === undefined) {
logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id })
continue
}
await activityProcessor(activity)
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,123 @@
import * as url from 'url'
import { database as db } from '../initializers'
import { logger } from './logger'
import { doRequest } from './requests'
import { isRemoteAccountValid } from './custom-validators'
import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
import { ResultList } from '../../shared/models/result-list.model'
async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
const options = {
uri: accountUrl,
method: 'GET'
}
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warning('Cannot fetch remote account %s.', accountUrl, err)
return undefined
}
const accountJSON: ActivityPubActor = requestResult.body
if (isRemoteAccountValid(accountJSON) === false) return undefined
const followersCount = await fetchAccountCount(accountJSON.followers)
const followingCount = await fetchAccountCount(accountJSON.following)
const account = db.Account.build({
uuid: accountJSON.uuid,
name: accountJSON.preferredUsername,
url: accountJSON.url,
publicKey: accountJSON.publicKey.publicKeyPem,
privateKey: null,
followersCount: followersCount,
followingCount: followingCount,
inboxUrl: accountJSON.inbox,
outboxUrl: accountJSON.outbox,
sharedInboxUrl: accountJSON.endpoints.sharedInbox,
followersUrl: accountJSON.followers,
followingUrl: accountJSON.following
})
const accountHost = url.parse(account.url).host
const podOptions = {
where: {
host: accountHost
},
defaults: {
host: accountHost
}
}
const pod = await db.Pod.findOrCreate(podOptions)
return { account, pod }
}
function activityPubContextify (data: object) {
return Object.assign(data,{
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{
'Hashtag': 'as:Hashtag',
'uuid': 'http://schema.org/identifier',
'category': 'http://schema.org/category',
'licence': 'http://schema.org/license',
'nsfw': 'as:sensitive',
'language': 'http://schema.org/inLanguage',
'views': 'http://schema.org/Number',
'size': 'http://schema.org/Number'
}
]
})
}
function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
const baseUrl = url.split('?').shift
const obj = {
id: baseUrl,
type: 'Collection',
totalItems: result.total,
first: {
id: baseUrl + '?page=' + page,
type: 'CollectionPage',
totalItems: result.total,
next: baseUrl + '?page=' + (page + 1),
partOf: baseUrl,
items: result.data
}
}
return activityPubContextify(obj)
}
// ---------------------------------------------------------------------------
export {
fetchRemoteAccountAndCreatePod,
activityPubContextify,
activityPubCollectionPagination
}
// ---------------------------------------------------------------------------
async function fetchAccountCount (url: string) {
const options = {
uri: url,
method: 'GET'
}
let requestResult
try {
requestResult = await doRequest(options)
} catch (err) {
logger.warning('Cannot fetch remote account count %s.', url, err)
return undefined
}
return requestResult.totalItems ? requestResult.totalItems : 0
}

View file

@ -19,8 +19,10 @@ import * as mkdirp from 'mkdirp'
import * as bcrypt from 'bcrypt'
import * as createTorrent from 'create-torrent'
import * as rimraf from 'rimraf'
import * as openssl from 'openssl-wrapper'
import * as Promise from 'bluebird'
import * as pem from 'pem'
import * as jsonld from 'jsonld'
import * as jsig from 'jsonld-signatures'
jsig.use('jsonld', jsonld)
function isTestInstance () {
return process.env.NODE_ENV === 'test'
@ -54,6 +56,12 @@ function escapeHTML (stringParam) {
return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
}
function pageToStartAndCount (page: number, itemsPerPage: number) {
const start = (page - 1) * itemsPerPage
return { start, count: itemsPerPage }
}
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
return function promisified (): Promise<A> {
return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
@ -104,13 +112,16 @@ const readdirPromise = promisify1<string, string[]>(readdir)
const mkdirpPromise = promisify1<string, string>(mkdirp)
const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
const accessPromise = promisify1WithVoid<string | Buffer>(access)
const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec)
const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
const createTorrentPromise = promisify2<string, any, any>(createTorrent)
const rimrafPromise = promisify1WithVoid<string>(rimraf)
const statPromise = promisify1<string, Stats>(stat)
const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign)
const jsonldVerifyPromise = promisify2<object, object, object>(jsig.verify)
// ---------------------------------------------------------------------------
@ -118,9 +129,11 @@ export {
isTestInstance,
root,
escapeHTML,
pageToStartAndCount,
promisify0,
promisify1,
readdirPromise,
readFilePromise,
readFileBufferPromise,
@ -130,11 +143,14 @@ export {
mkdirpPromise,
pseudoRandomBytesPromise,
accessPromise,
opensslExecPromise,
createPrivateKey,
getPublicKey,
bcryptComparePromise,
bcryptGenSaltPromise,
bcryptHashPromise,
createTorrentPromise,
rimrafPromise,
statPromise
statPromise,
jsonldSignPromise,
jsonldVerifyPromise
}

View file

@ -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
}

View file

@ -0,0 +1,4 @@
export * from './account'
export * from './signature'
export * from './misc'
export * from './videos'

View file

@ -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
}

View file

@ -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
}

View file

@ -1,4 +1,4 @@
export * from './remote'
export * from './activitypub'
export * from './misc'
export * from './pods'
export * from './pods'

View file

@ -1 +0,0 @@
export * from './videos'

View file

@ -1,4 +1,3 @@
import * as Promise from 'bluebird'
import * as ffmpeg from 'fluent-ffmpeg'
import { CONFIG } from '../initializers'

View file

@ -1,3 +1,4 @@
export * from './activitypub'
export * from './core-utils'
export * from './logger'
export * from './custom-validators'
@ -6,3 +7,4 @@ export * from './database-utils'
export * from './peertube-crypto'
export * from './requests'
export * from './utils'
export * from './webfinger'

View file

@ -1,148 +1,82 @@
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)
async function createPrivateAndPublicKeys () {
logger.info('Generating a RSA key...')
let dataString
if (typeof data === 'string') {
dataString = data
} else {
try {
dataString = JSON.stringify(data)
} catch (err) {
logger.error('Cannot check signature.', err)
return false
}
}
const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE)
const { publicKey } = await getPublicKey(key)
verify.update(dataString, 'utf8')
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
}
return jsonldVerifyPromise(signedDocument, options)
.catch(err => {
logger.error('Cannot check signature.', err)
return false
})
}
function signObject (byAccount: AccountInstance, data: any) {
const options = {
privateKeyPem: byAccount.privateKey,
creator: byAccount.url
}
return jsonldSignPromise(data, options)
}
function comparePassword (plainPassword: string, hashPassword: string) {
return bcryptComparePromise(plainPassword, hashPassword)
}
async function createCertsIfNotExist () {
const exist = await certsExist()
if (exist === true) {
return
}
return createCerts()
}
async function cryptPassword (password: string) {
const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
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
}

View file

@ -9,7 +9,13 @@ import {
} from '../initializers'
import { PodInstance } from '../models'
import { PodSignature } from '../../shared'
import { sign } from './peertube-crypto'
import { signObject } from './peertube-crypto'
function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
})
}
type MakeRetryRequestParams = {
url: string,
@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) {
}
type MakeSecureRequestParams = {
method: 'GET' | 'POST'
toPod: PodInstance
path: string
data?: Object
}
function makeSecureRequest (params: MakeSecureRequestParams) {
return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
const requestParams: {
url: string,
json: {
signature: PodSignature,
data: any
}
} = {
url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
json: {
signature: null,
data: null
}
const requestParams: {
method: 'POST',
uri: string,
json: {
signature: PodSignature,
data: any
}
} = {
method: 'POST',
uri: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
json: {
signature: null,
data: null
}
}
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
}
sign(dataToSign).then(signature => {
requestParams.json.signature = {
host, // Which host we pretend to be
signature
}
if (params.method !== 'POST') {
return rej(new Error('Cannot make a secure request with a non POST method.'))
}
const host = CONFIG.WEBSERVER.HOST
let dataToSign
// If there are data information
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
requestParams.json.data = params.data
}
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
}
request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body }))
})
return doRequest(requestParams)
})
}
// ---------------------------------------------------------------------------
export {
doRequest,
makeRetryRequest,
makeSecureRequest
}

View file

@ -0,0 +1,44 @@
import * as WebFinger from 'webfinger.js'
import { isTestInstance } from './core-utils'
import { isActivityPubUrlValid } from './custom-validators'
import { WebFingerData } from '../../shared'
import { fetchRemoteAccountAndCreatePod } from './activitypub'
const webfinger = new WebFinger({
webfist_fallback: false,
tls_only: isTestInstance(),
uri_fallback: false,
request_timeout: 3000
})
async function getAccountFromWebfinger (url: string) {
const webfingerData: WebFingerData = await webfingerLookup(url)
if (Array.isArray(webfingerData.links) === false) return undefined
const selfLink = webfingerData.links.find(l => l.rel === 'self')
if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined
const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href)
return account
}
// ---------------------------------------------------------------------------
export {
getAccountFromWebfinger
}
// ---------------------------------------------------------------------------
function webfingerLookup (url: string) {
return new Promise<WebFingerData>((res, rej) => {
webfinger.lookup('nick@silverbucket.net', (err, p) => {
if (err) return rej(err)
return p
})
})
}

View file

@ -2,7 +2,7 @@ import * as config from 'config'
import { promisify0 } from '../helpers/core-utils'
import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
import { UserModel } from '../models/user/user-interface'
import { UserModel } from '../models/account/user-interface'
// Some checks on configuration files
function checkConfig () {

View file

@ -10,7 +10,8 @@ import {
RequestVideoEventType,
RequestVideoQaduType,
RemoteVideoRequestType,
JobState
JobState,
JobCategory
} from '../../shared/models'
import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
@ -60,7 +61,6 @@ const CONFIG = {
PASSWORD: config.get<string>('database.password')
},
STORAGE: {
CERT_DIR: join(root(), config.get<string>('storage.certs')),
LOG_DIR: join(root(), config.get<string>('storage.logs')),
VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')),
@ -211,6 +211,10 @@ const FRIEND_SCORE = {
MAX: 1000
}
const ACTIVITY_PUB = {
COLLECTION_ITEMS_PER_PAGE: 10
}
// ---------------------------------------------------------------------------
// Number of points we add/remove from a friend after a successful/bad request
@ -288,17 +292,23 @@ const JOB_STATES: { [ id: string ]: JobState } = {
ERROR: 'error',
SUCCESS: 'success'
}
const JOB_CATEGORIES: { [ id: string ]: JobCategory } = {
TRANSCODING: 'transcoding',
HTTP_REQUEST: 'http-request'
}
// How many maximum jobs we fetch from the database per cycle
const JOBS_FETCH_LIMIT_PER_CYCLE = 10
const JOBS_FETCH_LIMIT_PER_CYCLE = {
transcoding: 10,
httpRequest: 20
}
// 1 minutes
let JOBS_FETCHING_INTERVAL = 60000
// ---------------------------------------------------------------------------
const PRIVATE_CERT_NAME = 'peertube.key.pem'
const PUBLIC_CERT_NAME = 'peertube.pub'
const SIGNATURE_ALGORITHM = 'RSA-SHA256'
const SIGNATURE_ENCODING = 'hex'
// const SIGNATURE_ALGORITHM = 'RSA-SHA256'
// const SIGNATURE_ENCODING = 'hex'
const PRIVATE_RSA_KEY_SIZE = 2048
// Password encryption
const BCRYPT_SALT_SIZE = 10
@ -368,14 +378,13 @@ export {
JOB_STATES,
JOBS_FETCH_LIMIT_PER_CYCLE,
JOBS_FETCHING_INTERVAL,
JOB_CATEGORIES,
LAST_MIGRATION_VERSION,
OAUTH_LIFETIME,
OPENGRAPH_AND_OEMBED_COMMENT,
PAGINATION_COUNT_DEFAULT,
PODS_SCORE,
PREVIEWS_SIZE,
PRIVATE_CERT_NAME,
PUBLIC_CERT_NAME,
REMOTE_SCHEME,
REQUEST_ENDPOINT_ACTIONS,
REQUEST_ENDPOINTS,
@ -393,11 +402,11 @@ export {
REQUESTS_VIDEO_QADU_LIMIT_PODS,
RETRY_REQUESTS,
SEARCHABLE_COLUMNS,
SIGNATURE_ALGORITHM,
SIGNATURE_ENCODING,
PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,
STATIC_MAX_AGE,
STATIC_PATHS,
ACTIVITY_PUB,
THUMBNAILS_SIZE,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,

View file

@ -15,8 +15,9 @@ import { BlacklistedVideoModel } from './../models/video/video-blacklist-interfa
import { VideoFileModel } from './../models/video/video-file-interface'
import { VideoAbuseModel } from './../models/video/video-abuse-interface'
import { VideoChannelModel } from './../models/video/video-channel-interface'
import { UserModel } from './../models/user/user-interface'
import { UserVideoRateModel } from './../models/user/user-video-rate-interface'
import { UserModel } from '../models/account/user-interface'
import { AccountVideoRateModel } from '../models/account/account-video-rate-interface'
import { AccountFollowModel } from '../models/account/account-follow-interface'
import { TagModel } from './../models/video/tag-interface'
import { RequestModel } from './../models/request/request-interface'
import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface'
@ -26,7 +27,7 @@ import { PodModel } from './../models/pod/pod-interface'
import { OAuthTokenModel } from './../models/oauth/oauth-token-interface'
import { OAuthClientModel } from './../models/oauth/oauth-client-interface'
import { JobModel } from './../models/job/job-interface'
import { AuthorModel } from './../models/video/author-interface'
import { AccountModel } from './../models/account/account-interface'
import { ApplicationModel } from './../models/application/application-interface'
const dbname = CONFIG.DATABASE.DBNAME
@ -38,7 +39,7 @@ const database: {
init?: (silent: boolean) => Promise<void>,
Application?: ApplicationModel,
Author?: AuthorModel,
Account?: AccountModel,
Job?: JobModel,
OAuthClient?: OAuthClientModel,
OAuthToken?: OAuthTokenModel,
@ -48,7 +49,8 @@ const database: {
RequestVideoQadu?: RequestVideoQaduModel,
Request?: RequestModel,
Tag?: TagModel,
UserVideoRate?: UserVideoRateModel,
AccountVideoRate?: AccountVideoRateModel,
AccountFollow?: AccountFollowModel,
User?: UserModel,
VideoAbuse?: VideoAbuseModel,
VideoChannel?: VideoChannelModel,
@ -126,7 +128,7 @@ async function getModelFiles (modelDirectory: string) {
return true
})
const tasks: Bluebird<any>[] = []
const tasks: Promise<any>[] = []
// For each directory we read it and append model in the modelFilePaths array
for (const directory of directories) {

View file

@ -0,0 +1,3 @@
export * from './process-create'
export * from './process-flag'
export * from './process-update'

View file

@ -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) {
}

View file

@ -0,0 +1,17 @@
import {
ActivityCreate,
VideoTorrentObject,
VideoChannelObject
} from '../../../shared'
function processFlagActivity (activity: ActivityCreate) {
// empty
}
// ---------------------------------------------------------------------------
export {
processFlagActivity
}
// ---------------------------------------------------------------------------

View file

@ -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) {
}

View file

@ -0,0 +1,129 @@
import * as Sequelize from 'sequelize'
import {
AccountInstance,
VideoInstance,
VideoChannelInstance
} from '../../models'
import { httpRequestJobScheduler } from '../jobs'
import { signObject, activityPubContextify } from '../../helpers'
import { Activity } from '../../../shared'
function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject()
const data = createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
return broadcastToFollowers(data, t)
}
function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject()
const data = updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
return broadcastToFollowers(data, t)
}
function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject()
const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
return broadcastToFollowers(data, t)
}
function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
const videoObject = video.toActivityPubObject()
const data = addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
return broadcastToFollowers(data, t)
}
function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
const videoObject = video.toActivityPubObject()
const data = updateActivityData(video.url, video.VideoChannel.Account, videoObject)
return broadcastToFollowers(data, t)
}
function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
const videoObject = video.toActivityPubObject()
const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject)
return broadcastToFollowers(data, t)
}
// ---------------------------------------------------------------------------
export {
}
// ---------------------------------------------------------------------------
function broadcastToFollowers (data: any, t: Sequelize.Transaction) {
return httpRequestJobScheduler.createJob(t, 'http-request', 'httpRequestBroadcastHandler', data)
}
function buildSignedActivity (byAccount: AccountInstance, data: Object) {
const activity = activityPubContextify(data)
return signObject(byAccount, activity) as Promise<Activity>
}
async function getPublicActivityTo (account: AccountInstance) {
const inboxUrls = await account.getFollowerSharedInboxUrls()
return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public')
}
async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
const to = await getPublicActivityTo(byAccount)
const base = {
type: 'Create',
id: url,
actor: byAccount.url,
to,
object
}
return buildSignedActivity(byAccount, base)
}
async function updateActivityData (url: string, byAccount: AccountInstance, object: any) {
const to = await getPublicActivityTo(byAccount)
const base = {
type: 'Update',
id: url,
actor: byAccount.url,
to,
object
}
return buildSignedActivity(byAccount, base)
}
async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) {
const to = await getPublicActivityTo(byAccount)
const base = {
type: 'Update',
id: url,
actor: byAccount.url,
to,
object
}
return buildSignedActivity(byAccount, base)
}
async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) {
const to = await getPublicActivityTo(byAccount)
const base = {
type: 'Add',
id: url,
actor: byAccount.url,
to,
object,
target
}
return buildSignedActivity(byAccount, base)
}

View file

@ -1,3 +1,4 @@
export * from './activitypub'
export * from './cache'
export * from './jobs'
export * from './request'

View file

@ -1,17 +0,0 @@
import * as videoFileOptimizer from './video-file-optimizer'
import * as videoFileTranscoder from './video-file-transcoder'
export interface JobHandler<T> {
process (data: object, jobId: number): T
onError (err: Error, jobId: number)
onSuccess (jobId: number, jobResult: T)
}
const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
videoFileOptimizer,
videoFileTranscoder
}
export {
jobHandlers
}

View file

@ -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
}

View file

@ -0,0 +1,17 @@
import { JobScheduler, JobHandler } from '../job-scheduler'
import * as httpRequestBroadcastHandler from './http-request-broadcast-handler'
import * as httpRequestUnicastHandler from './http-request-unicast-handler'
import { JobCategory } from '../../../../shared'
const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
httpRequestBroadcastHandler,
httpRequestUnicastHandler
}
const jobCategory: JobCategory = 'http-request'
const httpRequestJobScheduler = new JobScheduler(jobCategory, jobHandlers)
export {
httpRequestJobScheduler
}

View file

@ -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
}

View file

@ -0,0 +1 @@
export * from './http-request-job-scheduler'

View file

@ -1 +1,2 @@
export * from './job-scheduler'
export * from './http-request-job-scheduler'
export * from './transcoding-job-scheduler'

View file

@ -1,39 +1,41 @@
import { AsyncQueue, forever, queue } from 'async'
import * as Sequelize from 'sequelize'
import { database as db } from '../../initializers/database'
import {
database as db,
JOBS_FETCHING_INTERVAL,
JOBS_FETCH_LIMIT_PER_CYCLE,
JOB_STATES
} from '../../initializers'
import { logger } from '../../helpers'
import { JobInstance } from '../../models'
import { JobHandler, jobHandlers } from './handlers'
import { JobCategory } from '../../../shared'
export interface JobHandler<T> {
process (data: object, jobId: number): T
onError (err: Error, jobId: number)
onSuccess (jobId: number, jobResult: T)
}
type JobQueueCallback = (err: Error) => void
class JobScheduler {
class JobScheduler<T> {
private static instance: JobScheduler
private constructor () { }
static get Instance () {
return this.instance || (this.instance = new this())
}
constructor (
private jobCategory: JobCategory,
private jobHandlers: { [ id: string ]: JobHandler<T> }
) {}
async activate () {
const limit = JOBS_FETCH_LIMIT_PER_CYCLE
const limit = JOBS_FETCH_LIMIT_PER_CYCLE[this.jobCategory]
logger.info('Jobs scheduler activated.')
logger.info('Jobs scheduler %s activated.', this.jobCategory)
const jobsQueue = queue<JobInstance, JobQueueCallback>(this.processJob.bind(this))
// Finish processing jobs from a previous start
const state = JOB_STATES.PROCESSING
try {
const jobs = await db.Job.listWithLimit(limit, state)
const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
this.enqueueJobs(jobsQueue, jobs)
} catch (err) {
@ -49,7 +51,7 @@ class JobScheduler {
const state = JOB_STATES.PENDING
try {
const jobs = await db.Job.listWithLimit(limit, state)
const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
this.enqueueJobs(jobsQueue, jobs)
} catch (err) {
@ -64,9 +66,10 @@ class JobScheduler {
)
}
createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object) {
createJob (transaction: Sequelize.Transaction, category: JobCategory, handlerName: string, handlerInputData: object) {
const createQuery = {
state: JOB_STATES.PENDING,
category,
handlerName,
handlerInputData
}
@ -80,7 +83,7 @@ class JobScheduler {
}
private async processJob (job: JobInstance, callback: (err: Error) => void) {
const jobHandler = jobHandlers[job.handlerName]
const jobHandler = this.jobHandlers[job.handlerName]
if (jobHandler === undefined) {
logger.error('Unknown job handler for job %s.', job.handlerName)
return callback(null)

View file

@ -0,0 +1 @@
export * from './transcoding-job-scheduler'

View file

@ -0,0 +1,17 @@
import { JobScheduler, JobHandler } from '../job-scheduler'
import * as videoFileOptimizer from './video-file-optimizer-handler'
import * as videoFileTranscoder from './video-file-transcoder-handler'
import { JobCategory } from '../../../../shared'
const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
videoFileOptimizer,
videoFileTranscoder
}
const jobCategory: JobCategory = 'transcoding'
const transcodingJobScheduler = new JobScheduler(jobCategory, jobHandlers)
export {
transcodingJobScheduler
}

View file

@ -1,9 +1,9 @@
import { database as db } from '../initializers'
import { UserInstance } from '../models'
import { addVideoAuthorToFriends } from './friends'
import { addVideoAccountToFriends } from './friends'
import { createVideoChannel } from './video-channel'
async function createUserAuthorAndChannel (user: UserInstance, validateUser = true) {
async function createUserAccountAndChannel (user: UserInstance, validateUser = true) {
const res = await db.sequelize.transaction(async t => {
const userOptions = {
transaction: t,
@ -11,25 +11,25 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
}
const userCreated = await user.save(userOptions)
const authorInstance = db.Author.build({
const accountInstance = db.Account.build({
name: userCreated.username,
podId: null, // It is our pod
userId: userCreated.id
})
const authorCreated = await authorInstance.save({ transaction: t })
const accountCreated = await accountInstance.save({ transaction: t })
const remoteVideoAuthor = authorCreated.toAddRemoteJSON()
const remoteVideoAccount = accountCreated.toAddRemoteJSON()
// Now we'll add the video channel's meta data to our friends
const author = await addVideoAuthorToFriends(remoteVideoAuthor, t)
const account = await addVideoAccountToFriends(remoteVideoAccount, t)
const videoChannelInfo = {
name: `Default ${userCreated.username} channel`
}
const videoChannel = await createVideoChannel(videoChannelInfo, authorCreated, t)
const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
return { author, videoChannel }
return { account, videoChannel }
})
return res
@ -38,5 +38,5 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
// ---------------------------------------------------------------------------
export {
createUserAuthorAndChannel
createUserAccountAndChannel
}

View file

@ -3,15 +3,15 @@ import * as Sequelize from 'sequelize'
import { addVideoChannelToFriends } from './friends'
import { database as db } from '../initializers'
import { logger } from '../helpers'
import { AuthorInstance } from '../models'
import { AccountInstance } from '../models'
import { VideoChannelCreate } from '../../shared/models'
async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) {
async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) {
const videoChannelData = {
name: videoChannelInfo.name,
description: videoChannelInfo.description,
remote: false,
authorId: author.id
authorId: account.id
}
const videoChannel = db.VideoChannel.build(videoChannelData)
@ -19,8 +19,8 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author:
const videoChannelCreated = await videoChannel.save(options)
// Do not forget to add Author information to the created video channel
videoChannelCreated.Author = author
// Do not forget to add Account information to the created video channel
videoChannelCreated.Account = account
const remoteVideoChannel = videoChannelCreated.toAddRemoteJSON()

View file

@ -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
}

View file

@ -1,9 +1,9 @@
export * from './validators'
export * from './activitypub'
export * from './async'
export * from './oauth'
export * from './pagination'
export * from './pods'
export * from './search'
export * from './secure'
export * from './sort'
export * from './user-right'

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -1,5 +1,6 @@
export * from './account'
export * from './oembed'
export * from './remote'
export * from './activitypub'
export * from './pagination'
export * from './pods'
export * from './sort'

View file

@ -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
}

View file

@ -0,0 +1,23 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
export namespace AccountFollowMethods {
}
export interface AccountFollowClass {
}
export interface AccountFollowAttributes {
accountId: number
targetAccountId: number
}
export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
id: number
createdAt: Date
updatedAt: Date
}
export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}

View file

@ -0,0 +1,56 @@
import * as Sequelize from 'sequelize'
import { addMethodsToModel } from '../utils'
import {
AccountFollowInstance,
AccountFollowAttributes,
AccountFollowMethods
} from './account-follow-interface'
let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
{ },
{
indexes: [
{
fields: [ 'accountId' ],
unique: true
},
{
fields: [ 'targetAccountId' ],
unique: true
}
]
}
)
const classMethods = [
associate
]
addMethodsToModel(AccountFollow, classMethods)
return AccountFollow
}
// ------------------------------ STATICS ------------------------------
function associate (models) {
AccountFollow.belongsTo(models.Account, {
foreignKey: {
name: 'accountId',
allowNull: false
},
onDelete: 'CASCADE'
})
AccountFollow.belongsTo(models.Account, {
foreignKey: {
name: 'targetAccountId',
allowNull: false
},
onDelete: 'CASCADE'
})
}

View file

@ -0,0 +1,74 @@
import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
import { PodInstance } from '../pod/pod-interface'
import { VideoChannelInstance } from '../video/video-channel-interface'
import { ActivityPubActor } from '../../../shared'
import { ResultList } from '../../../shared/models/result-list.model'
export namespace AccountMethods {
export type Load = (id: number) => Bluebird<AccountInstance>
export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
export type ListOwned = () => Bluebird<AccountInstance[]>
export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
export type IsOwned = (this: AccountInstance) => boolean
export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
export type GetFollowingUrl = (this: AccountInstance) => string
export type GetFollowersUrl = (this: AccountInstance) => string
export type GetPublicKeyUrl = (this: AccountInstance) => string
}
export interface AccountClass {
loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
load: AccountMethods.Load
loadByUUID: AccountMethods.LoadByUUID
loadByUrl: AccountMethods.LoadByUrl
loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
listOwned: AccountMethods.ListOwned
listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
}
export interface AccountAttributes {
name: string
url: string
publicKey: string
privateKey: string
followersCount: number
followingCount: number
inboxUrl: string
outboxUrl: string
sharedInboxUrl: string
followersUrl: string
followingUrl: string
uuid?: string
podId?: number
userId?: number
applicationId?: number
}
export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
isOwned: AccountMethods.IsOwned
toActivityPubObject: AccountMethods.ToActivityPubObject
getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
getFollowingUrl: AccountMethods.GetFollowingUrl
getFollowersUrl: AccountMethods.GetFollowersUrl
getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
id: number
createdAt: Date
updatedAt: Date
Pod: PodInstance
VideoChannels: VideoChannelInstance[]
}
export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}

View file

@ -0,0 +1,26 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
export namespace AccountVideoRateMethods {
export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
}
export interface AccountVideoRateClass {
load: AccountVideoRateMethods.Load
}
export interface AccountVideoRateAttributes {
type: VideoRateType
accountId: number
videoId: number
}
export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
id: number
createdAt: Date
updatedAt: Date
}
export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}

View file

@ -1,5 +1,5 @@
/*
User rates per video.
Account rates per video.
*/
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
@ -8,17 +8,17 @@ import { VIDEO_RATE_TYPES } from '../../initializers'
import { addMethodsToModel } from '../utils'
import {
UserVideoRateInstance,
UserVideoRateAttributes,
AccountVideoRateInstance,
AccountVideoRateAttributes,
UserVideoRateMethods
} from './user-video-rate-interface'
AccountVideoRateMethods
} from './account-video-rate-interface'
let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes>
let load: UserVideoRateMethods.Load
let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
let load: AccountVideoRateMethods.Load
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
UserVideoRate = sequelize.define<UserVideoRateInstance, UserVideoRateAttributes>('UserVideoRate',
AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
{
type: {
type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
@ -28,7 +28,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
{
indexes: [
{
fields: [ 'videoId', 'userId', 'type' ],
fields: [ 'videoId', 'accountId', 'type' ],
unique: true
}
]
@ -40,15 +40,15 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
load
]
addMethodsToModel(UserVideoRate, classMethods)
addMethodsToModel(AccountVideoRate, classMethods)
return UserVideoRate
return AccountVideoRate
}
// ------------------------------ STATICS ------------------------------
function associate (models) {
UserVideoRate.belongsTo(models.Video, {
AccountVideoRate.belongsTo(models.Video, {
foreignKey: {
name: 'videoId',
allowNull: false
@ -56,23 +56,23 @@ function associate (models) {
onDelete: 'CASCADE'
})
UserVideoRate.belongsTo(models.User, {
AccountVideoRate.belongsTo(models.Account, {
foreignKey: {
name: 'userId',
name: 'accountId',
allowNull: false
},
onDelete: 'CASCADE'
})
}
load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) {
const options: Sequelize.FindOptions<UserVideoRateAttributes> = {
load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) {
const options: Sequelize.FindOptions<AccountVideoRateAttributes> = {
where: {
userId,
accountId,
videoId
}
}
if (transaction) options.transaction = transaction
return UserVideoRate.findOne(options)
return AccountVideoRate.findOne(options)
}

View file

@ -0,0 +1,444 @@
import * as Sequelize from 'sequelize'
import {
isUserUsernameValid,
isAccountPublicKeyValid,
isAccountUrlValid,
isAccountPrivateKeyValid,
isAccountFollowersCountValid,
isAccountFollowingCountValid,
isAccountInboxValid,
isAccountOutboxValid,
isAccountSharedInboxValid,
isAccountFollowersValid,
isAccountFollowingValid,
activityPubContextify
} from '../../helpers'
import { addMethodsToModel } from '../utils'
import {
AccountInstance,
AccountAttributes,
AccountMethods
} from './account-interface'
let Account: Sequelize.Model<AccountInstance, AccountAttributes>
let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
let load: AccountMethods.Load
let loadByUUID: AccountMethods.LoadByUUID
let loadByUrl: AccountMethods.LoadByUrl
let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
let listOwned: AccountMethods.ListOwned
let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
let isOwned: AccountMethods.IsOwned
let toActivityPubObject: AccountMethods.ToActivityPubObject
let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
let getFollowingUrl: AccountMethods.GetFollowingUrl
let getFollowersUrl: AccountMethods.GetFollowersUrl
let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
{
uuid: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
validate: {
isUUID: 4
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
usernameValid: value => {
const res = isUserUsernameValid(value)
if (res === false) throw new Error('Username is not valid.')
}
}
},
url: {
type: DataTypes.STRING,
allowNull: false,
validate: {
urlValid: value => {
const res = isAccountUrlValid(value)
if (res === false) throw new Error('URL is not valid.')
}
}
},
publicKey: {
type: DataTypes.STRING,
allowNull: false,
validate: {
publicKeyValid: value => {
const res = isAccountPublicKeyValid(value)
if (res === false) throw new Error('Public key is not valid.')
}
}
},
privateKey: {
type: DataTypes.STRING,
allowNull: false,
validate: {
privateKeyValid: value => {
const res = isAccountPrivateKeyValid(value)
if (res === false) throw new Error('Private key is not valid.')
}
}
},
followersCount: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
followersCountValid: value => {
const res = isAccountFollowersCountValid(value)
if (res === false) throw new Error('Followers count is not valid.')
}
}
},
followingCount: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
followersCountValid: value => {
const res = isAccountFollowingCountValid(value)
if (res === false) throw new Error('Following count is not valid.')
}
}
},
inboxUrl: {
type: DataTypes.STRING,
allowNull: false,
validate: {
inboxUrlValid: value => {
const res = isAccountInboxValid(value)
if (res === false) throw new Error('Inbox URL is not valid.')
}
}
},
outboxUrl: {
type: DataTypes.STRING,
allowNull: false,
validate: {
outboxUrlValid: value => {
const res = isAccountOutboxValid(value)
if (res === false) throw new Error('Outbox URL is not valid.')
}
}
},
sharedInboxUrl: {
type: DataTypes.STRING,
allowNull: false,
validate: {
sharedInboxUrlValid: value => {
const res = isAccountSharedInboxValid(value)
if (res === false) throw new Error('Shared inbox URL is not valid.')
}
}
},
followersUrl: {
type: DataTypes.STRING,
allowNull: false,
validate: {
followersUrlValid: value => {
const res = isAccountFollowersValid(value)
if (res === false) throw new Error('Followers URL is not valid.')
}
}
},
followingUrl: {
type: DataTypes.STRING,
allowNull: false,
validate: {
followingUrlValid: value => {
const res = isAccountFollowingValid(value)
if (res === false) throw new Error('Following URL is not valid.')
}
}
}
},
{
indexes: [
{
fields: [ 'name' ]
},
{
fields: [ 'podId' ]
},
{
fields: [ 'userId' ],
unique: true
},
{
fields: [ 'applicationId' ],
unique: true
},
{
fields: [ 'name', 'podId' ],
unique: true
}
],
hooks: { afterDestroy }
}
)
const classMethods = [
associate,
loadAccountByPodAndUUID,
load,
loadByUUID,
loadLocalAccountByName,
listOwned,
listFollowerUrlsForApi,
listFollowingUrlsForApi
]
const instanceMethods = [
isOwned,
toActivityPubObject,
getFollowerSharedInboxUrls,
getFollowingUrl,
getFollowersUrl,
getPublicKeyUrl
]
addMethodsToModel(Account, classMethods, instanceMethods)
return Account
}
// ---------------------------------------------------------------------------
function associate (models) {
Account.belongsTo(models.Pod, {
foreignKey: {
name: 'podId',
allowNull: true
},
onDelete: 'cascade'
})
Account.belongsTo(models.User, {
foreignKey: {
name: 'userId',
allowNull: true
},
onDelete: 'cascade'
})
Account.belongsTo(models.Application, {
foreignKey: {
name: 'userId',
allowNull: true
},
onDelete: 'cascade'
})
Account.hasMany(models.VideoChannel, {
foreignKey: {
name: 'accountId',
allowNull: false
},
onDelete: 'cascade',
hooks: true
})
Account.hasMany(models.AccountFollower, {
foreignKey: {
name: 'accountId',
allowNull: false
},
onDelete: 'cascade'
})
Account.hasMany(models.AccountFollower, {
foreignKey: {
name: 'targetAccountId',
allowNull: false
},
onDelete: 'cascade'
})
}
function afterDestroy (account: AccountInstance) {
if (account.isOwned()) {
const removeVideoAccountToFriendsParams = {
uuid: account.uuid
}
return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
}
return undefined
}
toActivityPubObject = function (this: AccountInstance) {
const type = this.podId ? 'Application' : 'Person'
const json = {
type,
id: this.url,
following: this.getFollowingUrl(),
followers: this.getFollowersUrl(),
inbox: this.inboxUrl,
outbox: this.outboxUrl,
preferredUsername: this.name,
url: this.url,
name: this.name,
endpoints: {
sharedInbox: this.sharedInboxUrl
},
uuid: this.uuid,
publicKey: {
id: this.getPublicKeyUrl(),
owner: this.url,
publicKeyPem: this.publicKey
}
}
return activityPubContextify(json)
}
isOwned = function (this: AccountInstance) {
return this.podId === null
}
getFollowerSharedInboxUrls = function (this: AccountInstance) {
const query: Sequelize.FindOptions<AccountAttributes> = {
attributes: [ 'sharedInboxUrl' ],
include: [
{
model: Account['sequelize'].models.AccountFollower,
where: {
targetAccountId: this.id
}
}
]
}
return Account.findAll(query)
.then(accounts => accounts.map(a => a.sharedInboxUrl))
}
getFollowingUrl = function (this: AccountInstance) {
return this.url + '/followers'
}
getFollowersUrl = function (this: AccountInstance) {
return this.url + '/followers'
}
getPublicKeyUrl = function (this: AccountInstance) {
return this.url + '#main-key'
}
// ------------------------------ STATICS ------------------------------
listOwned = function () {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
podId: null
}
}
return Account.findAll(query)
}
listFollowerUrlsForApi = function (name: string, start: number, count: number) {
return createListFollowForApiQuery('followers', name, start, count)
}
listFollowingUrlsForApi = function (name: string, start: number, count: number) {
return createListFollowForApiQuery('following', name, start, count)
}
load = function (id: number) {
return Account.findById(id)
}
loadByUUID = function (uuid: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
uuid
}
}
return Account.findOne(query)
}
loadLocalAccountByName = function (name: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
name,
userId: {
[Sequelize.Op.ne]: null
}
}
}
return Account.findOne(query)
}
loadByUrl = function (url: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
url
}
}
return Account.findOne(query)
}
loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
podId,
uuid
},
transaction
}
return Account.find(query)
}
// ------------------------------ UTILS ------------------------------
async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) {
let firstJoin: string
let secondJoin: string
if (type === 'followers') {
firstJoin = 'targetAccountId'
secondJoin = 'accountId'
} else {
firstJoin = 'accountId'
secondJoin = 'targetAccountId'
}
const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ]
const tasks: Promise<any>[] = []
for (const selection of selections) {
const query = 'SELECT ' + selection + ' FROM "Account" ' +
'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' +
'WHERE "Account"."name" = \'$name\' ' +
'LIMIT ' + start + ', ' + count
const options = {
bind: { name },
type: Sequelize.QueryTypes.SELECT
}
tasks.push(Account['sequelize'].query(query, options))
}
const [ followers, [ { total } ]] = await Promise.all(tasks)
const urls: string[] = followers.map(f => f.url)
return {
data: urls,
total: parseInt(total, 10)
}
}

View file

@ -0,0 +1,4 @@
export * from './account-interface'
export * from './account-follow-interface'
export * from './account-video-rate-interface'
export * from './user-interface'

View file

@ -1,10 +1,10 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import * as Bluebird from 'bluebird'
// Don't use barrel, import just what we need
import { AccountInstance } from './account-interface'
import { User as FormattedUser } from '../../../shared/models/users/user.model'
import { ResultList } from '../../../shared/models/result-list.model'
import { AuthorInstance } from '../video/author-interface'
import { UserRight } from '../../../shared/models/users/user-right.enum'
import { UserRole } from '../../../shared/models/users/user-role'
@ -15,18 +15,18 @@ export namespace UserMethods {
export type ToFormattedJSON = (this: UserInstance) => FormattedUser
export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
export type CountTotal = () => Promise<number>
export type CountTotal = () => Bluebird<number>
export type GetByUsername = (username: string) => Promise<UserInstance>
export type GetByUsername = (username: string) => Bluebird<UserInstance>
export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> >
export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
export type LoadById = (id: number) => Promise<UserInstance>
export type LoadById = (id: number) => Bluebird<UserInstance>
export type LoadByUsername = (username: string) => Promise<UserInstance>
export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance>
export type LoadByUsername = (username: string) => Bluebird<UserInstance>
export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance>
export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
}
export interface UserClass {
@ -53,7 +53,7 @@ export interface UserAttributes {
role: UserRole
videoQuota: number
Author?: AuthorInstance
Account?: AccountInstance
}
export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {

View file

@ -1,5 +1,4 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { getSort, addMethodsToModel } from '../utils'
import {
@ -166,13 +165,13 @@ toFormattedJSON = function (this: UserInstance) {
videoQuota: this.videoQuota,
createdAt: this.createdAt,
author: {
id: this.Author.id,
uuid: this.Author.uuid
id: this.Account.id,
uuid: this.Account.uuid
}
}
if (Array.isArray(this.Author.VideoChannels) === true) {
const videoChannels = this.Author.VideoChannels
if (Array.isArray(this.Account.VideoChannels) === true) {
const videoChannels = this.Account.VideoChannels
.map(c => c.toFormattedJSON())
.sort((v1, v2) => {
if (v1.createdAt < v2.createdAt) return -1
@ -198,7 +197,7 @@ isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.Fi
// ------------------------------ STATICS ------------------------------
function associate (models) {
User.hasOne(models.Author, {
User.hasOne(models.Account, {
foreignKey: 'userId',
onDelete: 'cascade'
})
@ -218,7 +217,7 @@ getByUsername = function (username: string) {
where: {
username: username
},
include: [ { model: User['sequelize'].models.Author, required: true } ]
include: [ { model: User['sequelize'].models.Account, required: true } ]
}
return User.findOne(query)
@ -229,7 +228,7 @@ listForApi = function (start: number, count: number, sort: string) {
offset: start,
limit: count,
order: [ getSort(sort) ],
include: [ { model: User['sequelize'].models.Author, required: true } ]
include: [ { model: User['sequelize'].models.Account, required: true } ]
}
return User.findAndCountAll(query).then(({ rows, count }) => {
@ -242,7 +241,7 @@ listForApi = function (start: number, count: number, sort: string) {
loadById = function (id: number) {
const options = {
include: [ { model: User['sequelize'].models.Author, required: true } ]
include: [ { model: User['sequelize'].models.Account, required: true } ]
}
return User.findById(id, options)
@ -253,7 +252,7 @@ loadByUsername = function (username: string) {
where: {
username
},
include: [ { model: User['sequelize'].models.Author, required: true } ]
include: [ { model: User['sequelize'].models.Account, required: true } ]
}
return User.findOne(query)
@ -266,7 +265,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
},
include: [
{
model: User['sequelize'].models.Author,
model: User['sequelize'].models.Account,
required: true,
include: [ User['sequelize'].models.VideoChannel ]
}
@ -278,7 +277,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
loadByUsernameOrEmail = function (username: string, email: string) {
const query = {
include: [ { model: User['sequelize'].models.Author, required: true } ],
include: [ { model: User['sequelize'].models.Account, required: true } ],
where: {
[Sequelize.Op.or]: [ { username }, { email } ]
}
@ -296,8 +295,8 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
'(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' +
'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' +
'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' +
'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
const options = {

View file

@ -3,5 +3,5 @@ export * from './job'
export * from './oauth'
export * from './pod'
export * from './request'
export * from './user'
export * from './account'
export * from './video'

View file

@ -1,14 +1,14 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { JobState } from '../../../shared/models/job.model'
import { JobCategory, JobState } from '../../../shared/models/job.model'
export namespace JobMethods {
export type ListWithLimit = (limit: number, state: JobState) => Promise<JobInstance[]>
export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
}
export interface JobClass {
listWithLimit: JobMethods.ListWithLimit
listWithLimitByCategory: JobMethods.ListWithLimitByCategory
}
export interface JobAttributes {

View file

@ -1,7 +1,7 @@
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
import { JOB_STATES } from '../../initializers'
import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
import { addMethodsToModel } from '../utils'
import {
@ -13,7 +13,7 @@ import {
import { JobState } from '../../../shared/models/job.model'
let Job: Sequelize.Model<JobInstance, JobAttributes>
let listWithLimit: JobMethods.ListWithLimit
let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Job = sequelize.define<JobInstance, JobAttributes>('Job',
@ -22,6 +22,10 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
type: DataTypes.ENUM(values(JOB_STATES)),
allowNull: false
},
category: {
type: DataTypes.ENUM(values(JOB_CATEGORIES)),
allowNull: false
},
handlerName: {
type: DataTypes.STRING,
allowNull: false
@ -40,7 +44,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
}
)
const classMethods = [ listWithLimit ]
const classMethods = [ listWithLimitByCategory ]
addMethodsToModel(Job, classMethods)
return Job
@ -48,7 +52,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
// ---------------------------------------------------------------------------
listWithLimit = function (limit: number, state: JobState) {
listWithLimitByCategory = function (limit: number, state: JobState) {
const query = {
order: [
[ 'id', 'ASC' ]

View file

@ -1,7 +1,7 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { UserModel } from '../user/user-interface'
import { UserModel } from '../account/user-interface'
export type OAuthTokenInfo = {
refreshToken: string

View file

@ -48,9 +48,7 @@ export interface PodClass {
export interface PodAttributes {
id?: number
host?: string
publicKey?: string
score?: number | Sequelize.literal // Sequelize literal for 'score +' + value
email?: string
}
export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> {

View file

@ -39,10 +39,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
}
}
},
publicKey: {
type: DataTypes.STRING(5000),
allowNull: false
},
score: {
type: DataTypes.INTEGER,
defaultValue: FRIEND_SCORE.BASE,
@ -51,13 +47,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
isInt: true,
max: FRIEND_SCORE.MAX
}
},
email: {
type: DataTypes.STRING(400),
allowNull: false,
validate: {
isEmail: true
}
}
},
{
@ -100,7 +89,6 @@ toFormattedJSON = function (this: PodInstance) {
const json = {
id: this.id,
host: this.host,
email: this.email,
score: this.score as number,
createdAt: this.createdAt
}

View file

@ -1,2 +0,0 @@
export * from './user-video-rate-interface'
export * from './user-interface'

View file

@ -1,26 +0,0 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
export namespace UserVideoRateMethods {
export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance>
}
export interface UserVideoRateClass {
load: UserVideoRateMethods.Load
}
export interface UserVideoRateAttributes {
type: VideoRateType
userId: number
videoId: number
}
export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance<UserVideoRateAttributes> {
id: number
createdAt: Date
updatedAt: Date
}
export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> {}

View file

@ -1,45 +0,0 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { PodInstance } from '../pod/pod-interface'
import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model'
import { VideoChannelInstance } from './video-channel-interface'
export namespace AuthorMethods {
export type Load = (id: number) => Promise<AuthorInstance>
export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
export type ListOwned = () => Promise<AuthorInstance[]>
export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
export type IsOwned = (this: AuthorInstance) => boolean
}
export interface AuthorClass {
loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
load: AuthorMethods.Load
loadByUUID: AuthorMethods.LoadByUUID
listOwned: AuthorMethods.ListOwned
}
export interface AuthorAttributes {
name: string
uuid?: string
podId?: number
userId?: number
}
export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> {
isOwned: AuthorMethods.IsOwned
toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
id: number
createdAt: Date
updatedAt: Date
Pod: PodInstance
VideoChannels: VideoChannelInstance[]
}
export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}

View file

@ -1,171 +0,0 @@
import * as Sequelize from 'sequelize'
import { isUserUsernameValid } from '../../helpers'
import { removeVideoAuthorToFriends } from '../../lib'
import { addMethodsToModel } from '../utils'
import {
AuthorInstance,
AuthorAttributes,
AuthorMethods
} from './author-interface'
let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
let load: AuthorMethods.Load
let loadByUUID: AuthorMethods.LoadByUUID
let listOwned: AuthorMethods.ListOwned
let isOwned: AuthorMethods.IsOwned
let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author',
{
uuid: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
allowNull: false,
validate: {
isUUID: 4
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
usernameValid: value => {
const res = isUserUsernameValid(value)
if (res === false) throw new Error('Username is not valid.')
}
}
}
},
{
indexes: [
{
fields: [ 'name' ]
},
{
fields: [ 'podId' ]
},
{
fields: [ 'userId' ],
unique: true
},
{
fields: [ 'name', 'podId' ],
unique: true
}
],
hooks: { afterDestroy }
}
)
const classMethods = [
associate,
loadAuthorByPodAndUUID,
load,
loadByUUID,
listOwned
]
const instanceMethods = [
isOwned,
toAddRemoteJSON
]
addMethodsToModel(Author, classMethods, instanceMethods)
return Author
}
// ---------------------------------------------------------------------------
function associate (models) {
Author.belongsTo(models.Pod, {
foreignKey: {
name: 'podId',
allowNull: true
},
onDelete: 'cascade'
})
Author.belongsTo(models.User, {
foreignKey: {
name: 'userId',
allowNull: true
},
onDelete: 'cascade'
})
Author.hasMany(models.VideoChannel, {
foreignKey: {
name: 'authorId',
allowNull: false
},
onDelete: 'cascade',
hooks: true
})
}
function afterDestroy (author: AuthorInstance) {
if (author.isOwned()) {
const removeVideoAuthorToFriendsParams = {
uuid: author.uuid
}
return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams)
}
return undefined
}
toAddRemoteJSON = function (this: AuthorInstance) {
const json = {
uuid: this.uuid,
name: this.name
}
return json
}
isOwned = function (this: AuthorInstance) {
return this.podId === null
}
// ------------------------------ STATICS ------------------------------
listOwned = function () {
const query: Sequelize.FindOptions<AuthorAttributes> = {
where: {
podId: null
}
}
return Author.findAll(query)
}
load = function (id: number) {
return Author.findById(id)
}
loadByUUID = function (uuid: string) {
const query: Sequelize.FindOptions<AuthorAttributes> = {
where: {
uuid
}
}
return Author.findOne(query)
}
loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
const query: Sequelize.FindOptions<AuthorAttributes> = {
where: {
podId,
uuid
},
transaction
}
return Author.find(query)
}

View file

@ -1,42 +1,42 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared'
import { ResultList } from '../../../shared'
// Don't use barrel, import just what we need
import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
import { AuthorInstance } from './author-interface'
import { VideoInstance } from './video-interface'
import { AccountInstance } from '../account/account-interface'
import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
export namespace VideoChannelMethods {
export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData
export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject
export type IsOwned = (this: VideoChannelInstance) => boolean
export type CountByAuthor = (authorId: number) => Promise<number>
export type CountByAccount = (accountId: number) => Promise<number>
export type ListOwned = () => Promise<VideoChannelInstance[]>
export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance>
export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> >
export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance>
export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance>
export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance>
export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> >
export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance>
export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance>
export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance>
export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
}
export interface VideoChannelClass {
countByAuthor: VideoChannelMethods.CountByAuthor
countByAccount: VideoChannelMethods.CountByAccount
listForApi: VideoChannelMethods.ListForApi
listByAuthor: VideoChannelMethods.ListByAuthor
listByAccount: VideoChannelMethods.ListByAccount
listOwned: VideoChannelMethods.ListOwned
loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
loadByUUID: VideoChannelMethods.LoadByUUID
loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
}
export interface VideoChannelAttributes {
@ -45,8 +45,9 @@ export interface VideoChannelAttributes {
name: string
description: string
remote: boolean
url: string
Author?: AuthorInstance
Account?: AccountInstance
Videos?: VideoInstance[]
}
@ -57,8 +58,7 @@ export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAtt
isOwned: VideoChannelMethods.IsOwned
toFormattedJSON: VideoChannelMethods.ToFormattedJSON
toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
toActivityPubObject: VideoChannelMethods.ToActivityPubObject
}
export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}

View file

@ -13,19 +13,18 @@ import {
let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON
let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
let isOwned: VideoChannelMethods.IsOwned
let countByAuthor: VideoChannelMethods.CountByAuthor
let countByAccount: VideoChannelMethods.CountByAccount
let listOwned: VideoChannelMethods.ListOwned
let listForApi: VideoChannelMethods.ListForApi
let listByAuthor: VideoChannelMethods.ListByAuthor
let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor
let listByAccount: VideoChannelMethods.ListByAccount
let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
let loadByUUID: VideoChannelMethods.LoadByUUID
let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor
let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor
let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos
let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
@ -62,12 +61,19 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
url: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isUrl: true
}
}
},
{
indexes: [
{
fields: [ 'authorId' ]
fields: [ 'accountId' ]
}
],
hooks: {
@ -80,21 +86,20 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
associate,
listForApi,
listByAuthor,
listByAccount,
listOwned,
loadByIdAndAuthor,
loadAndPopulateAuthor,
loadByUUIDAndPopulateAuthor,
loadByIdAndAccount,
loadAndPopulateAccount,
loadByUUIDAndPopulateAccount,
loadByUUID,
loadByHostAndUUID,
loadAndPopulateAuthorAndVideos,
countByAuthor
loadAndPopulateAccountAndVideos,
countByAccount
]
const instanceMethods = [
isOwned,
toFormattedJSON,
toAddRemoteJSON,
toUpdateRemoteJSON
toActivityPubObject,
]
addMethodsToModel(VideoChannel, classMethods, instanceMethods)
@ -118,10 +123,10 @@ toFormattedJSON = function (this: VideoChannelInstance) {
updatedAt: this.updatedAt
}
if (this.Author !== undefined) {
if (this.Account !== undefined) {
json['owner'] = {
name: this.Author.name,
uuid: this.Author.uuid
name: this.Account.name,
uuid: this.Account.uuid
}
}
@ -132,27 +137,14 @@ toFormattedJSON = function (this: VideoChannelInstance) {
return json
}
toAddRemoteJSON = function (this: VideoChannelInstance) {
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
}
return json
}
toUpdateRemoteJSON = 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

View file

@ -1,5 +1,5 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import * as Bluebird from 'bluebird'
import { TagAttributes, TagInstance } from './tag-interface'
import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/
import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
import { ResultList } from '../../../shared/models/result-list.model'
import { VideoChannelInstance } from './video-channel-interface'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
export namespace VideoMethods {
export type GetThumbnailName = (this: VideoInstance) => string
@ -29,8 +30,7 @@ export namespace VideoMethods {
export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData>
export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject
export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
@ -40,31 +40,35 @@ export namespace VideoMethods {
export type GetPreviewPath = (this: VideoInstance) => string
export type GetDescriptionPath = (this: VideoInstance) => string
export type GetTruncatedDescription = (this: VideoInstance) => string
export type GetCategoryLabel = (this: VideoInstance) => string
export type GetLicenceLabel = (this: VideoInstance) => string
export type GetLanguageLabel = (this: VideoInstance) => string
// Return thumbnail name
export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
export type List = () => Promise<VideoInstance[]>
export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]>
export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]>
export type List = () => Bluebird<VideoInstance[]>
export type ListOwnedAndPopulateAccountAndTags = () => Bluebird<VideoInstance[]>
export type ListOwnedByAccount = (account: string) => Bluebird<VideoInstance[]>
export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> >
export type SearchAndPopulateAuthorAndPodAndTags = (
export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
export type SearchAndPopulateAccountAndPodAndTags = (
value: string,
field: string,
start: number,
count: number,
sort: string
) => Promise< ResultList<VideoInstance> >
) => Bluebird< ResultList<VideoInstance> >
export type Load = (id: number) => Promise<VideoInstance>
export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance>
export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance>
export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance>
export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance>
export type Load = (id: number) => Bluebird<VideoInstance>
export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
export type RemovePreview = (this: VideoInstance) => Promise<void>
@ -77,16 +81,17 @@ export interface VideoClass {
list: VideoMethods.List
listForApi: VideoMethods.ListForApi
listUserVideosForApi: VideoMethods.ListUserVideosForApi
listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
listOwnedByAccount: VideoMethods.ListOwnedByAccount
load: VideoMethods.Load
loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
loadByUUID: VideoMethods.LoadByUUID
loadByUrl: VideoMethods.LoadByUrl
loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
}
export interface VideoAttributes {
@ -104,7 +109,9 @@ export interface VideoAttributes {
likes?: number
dislikes?: number
remote: boolean
url: string
parentId?: number
channelId?: number
VideoChannel?: VideoChannelInstance
@ -132,16 +139,18 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
removePreview: VideoMethods.RemovePreview
removeThumbnail: VideoMethods.RemoveThumbnail
removeTorrent: VideoMethods.RemoveTorrent
toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
toActivityPubObject: VideoMethods.ToActivityPubObject
toFormattedJSON: VideoMethods.ToFormattedJSON
toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
getEmbedPath: VideoMethods.GetEmbedPath
getDescriptionPath: VideoMethods.GetDescriptionPath
getTruncatedDescription: VideoMethods.GetTruncatedDescription
getCategoryLabel: VideoMethods.GetCategoryLabel
getLicenceLabel: VideoMethods.GetLicenceLabel
getLanguageLabel: VideoMethods.GetLanguageLabel
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
}
export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}

View file

@ -5,7 +5,6 @@ import { map, maxBy, truncate } from 'lodash'
import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { TagInstance } from './tag-interface'
import {
@ -52,6 +51,7 @@ import {
VideoMethods
} from './video-interface'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
let Video: Sequelize.Model<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile
@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName
let isOwned: VideoMethods.IsOwned
let toFormattedJSON: VideoMethods.ToFormattedJSON
let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
let toActivityPubObject: VideoMethods.ToActivityPubObject
let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
let createPreview: VideoMethods.CreatePreview
@ -76,21 +75,25 @@ let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
let getEmbedPath: VideoMethods.GetEmbedPath
let getDescriptionPath: VideoMethods.GetDescriptionPath
let getTruncatedDescription: VideoMethods.GetTruncatedDescription
let getCategoryLabel: VideoMethods.GetCategoryLabel
let getLicenceLabel: VideoMethods.GetLicenceLabel
let getLanguageLabel: VideoMethods.GetLanguageLabel
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let list: VideoMethods.List
let listForApi: VideoMethods.ListForApi
let listUserVideosForApi: VideoMethods.ListUserVideosForApi
let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
let listOwnedByAccount: VideoMethods.ListOwnedByAccount
let load: VideoMethods.Load
let loadByUUID: VideoMethods.LoadByUUID
let loadByUrl: VideoMethods.LoadByUrl
let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
let removeThumbnail: VideoMethods.RemoveThumbnail
let removePreview: VideoMethods.RemovePreview
let removeFile: VideoMethods.RemoveFile
@ -219,6 +222,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
url: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isUrl: true
}
}
},
{
@ -243,6 +253,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
},
{
fields: [ 'channelId' ]
},
{
fields: [ 'parentId' ]
}
],
hooks: {
@ -258,16 +271,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
list,
listForApi,
listUserVideosForApi,
listOwnedAndPopulateAuthorAndTags,
listOwnedByAuthor,
listOwnedAndPopulateAccountAndTags,
listOwnedByAccount,
load,
loadAndPopulateAuthor,
loadAndPopulateAuthorAndPodAndTags,
loadAndPopulateAccount,
loadAndPopulateAccountAndPodAndTags,
loadByHostAndUUID,
loadByUUID,
loadLocalVideoByUUID,
loadByUUIDAndPopulateAuthorAndPodAndTags,
searchAndPopulateAuthorAndPodAndTags
loadByUUIDAndPopulateAccountAndPodAndTags,
searchAndPopulateAccountAndPodAndTags
]
const instanceMethods = [
createPreview,
@ -286,16 +299,18 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
removePreview,
removeThumbnail,
removeTorrent,
toAddRemoteJSON,
toActivityPubObject,
toFormattedJSON,
toFormattedDetailsJSON,
toUpdateRemoteJSON,
optimizeOriginalVideofile,
transcodeOriginalVideofile,
getOriginalFileHeight,
getEmbedPath,
getTruncatedDescription,
getDescriptionPath
getDescriptionPath,
getCategoryLabel,
getLicenceLabel,
getLanguageLabel
]
addMethodsToModel(Video, classMethods, instanceMethods)
@ -313,6 +328,14 @@ function associate (models) {
onDelete: 'cascade'
})
Video.belongsTo(models.VideoChannel, {
foreignKey: {
name: 'parentId',
allowNull: true
},
onDelete: 'cascade'
})
Video.belongsToMany(models.Tag, {
foreignKey: 'videoId',
through: models.VideoTag,
@ -423,7 +446,7 @@ getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance)
return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
}
createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
const options = {
announceList: [
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
@ -433,18 +456,15 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil
]
}
return createTorrentPromise(this.getVideoFilePath(videoFile), options)
.then(torrent => {
const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
logger.info('Creating torrent %s.', filePath)
const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
return writeFilePromise(filePath, torrent).then(() => torrent)
})
.then(torrent => {
const parsedTorrent = parseTorrent(torrent)
const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
logger.info('Creating torrent %s.', filePath)
videoFile.infoHash = parsedTorrent.infoHash
})
await writeFilePromise(filePath, torrent)
const parsedTorrent = parseTorrent(torrent)
videoFile.infoHash = parsedTorrent.infoHash
}
getEmbedPath = function (this: VideoInstance) {
@ -462,40 +482,28 @@ getPreviewPath = function (this: VideoInstance) {
toFormattedJSON = function (this: VideoInstance) {
let podHost
if (this.VideoChannel.Author.Pod) {
podHost = this.VideoChannel.Author.Pod.host
if (this.VideoChannel.Account.Pod) {
podHost = this.VideoChannel.Account.Pod.host
} else {
// It means it's our video
podHost = CONFIG.WEBSERVER.HOST
}
// Maybe our pod is not up to date and there are new categories since our version
let categoryLabel = VIDEO_CATEGORIES[this.category]
if (!categoryLabel) categoryLabel = 'Misc'
// Maybe our pod is not up to date and there are new licences since our version
let licenceLabel = VIDEO_LICENCES[this.licence]
if (!licenceLabel) licenceLabel = 'Unknown'
// Language is an optional attribute
let languageLabel = VIDEO_LANGUAGES[this.language]
if (!languageLabel) languageLabel = 'Unknown'
const json = {
id: this.id,
uuid: this.uuid,
name: this.name,
category: this.category,
categoryLabel,
categoryLabel: this.getCategoryLabel(),
licence: this.licence,
licenceLabel,
licenceLabel: this.getLicenceLabel(),
language: this.language,
languageLabel,
languageLabel: this.getLanguageLabel(),
nsfw: this.nsfw,
description: this.getTruncatedDescription(),
podHost,
isLocal: this.isOwned(),
author: this.VideoChannel.Author.name,
account: this.VideoChannel.Account.name,
duration: this.duration,
views: this.views,
likes: this.likes,
@ -552,75 +560,75 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
return Object.assign(formattedJson, detailsJson)
}
toAddRemoteJSON = function (this: VideoInstance) {
// Get thumbnail data to send to the other pod
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
toActivityPubObject = function (this: VideoInstance) {
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
const remoteVideo = {
uuid: this.uuid,
name: this.name,
category: this.category,
licence: this.licence,
language: this.language,
nsfw: this.nsfw,
truncatedDescription: this.getTruncatedDescription(),
channelUUID: this.VideoChannel.uuid,
duration: this.duration,
thumbnailData: thumbnailData.toString('binary'),
tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
privacy: this.privacy,
files: []
}
const tag = this.Tags.map(t => ({
type: 'Hashtag',
name: t.name
}))
this.VideoFiles.forEach(videoFile => {
remoteVideo.files.push({
infoHash: videoFile.infoHash,
resolution: videoFile.resolution,
extname: videoFile.extname,
size: videoFile.size
})
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
})
return remoteVideo
})
}
url.push({
type: 'Link',
mimeType: 'application/x-bittorrent',
url: getTorrentUrl(this, file, baseUrlHttp),
width: file.resolution
})
toUpdateRemoteJSON = function (this: VideoInstance) {
const json = {
uuid: this.uuid,
name: this.name,
category: this.category,
licence: this.licence,
language: this.language,
nsfw: this.nsfw,
truncatedDescription: this.getTruncatedDescription(),
duration: this.duration,
tags: map<TagInstance, string>(this.Tags, 'name'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
views: this.views,
likes: this.likes,
dislikes: this.dislikes,
privacy: this.privacy,
files: []
url.push({
type: 'Link',
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
width: file.resolution
})
}
this.VideoFiles.forEach(videoFile => {
json.files.push({
infoHash: videoFile.infoHash,
resolution: videoFile.resolution,
extname: videoFile.extname,
size: videoFile.size
})
})
const videoObject: VideoTorrentObject = {
type: 'Video',
name: this.name,
// 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,
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
}
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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,9 @@
import { Activity } from './activity'
export interface ActivityPubCollection {
'@context': string[]
type: 'Collection' | 'CollectionPage'
totalItems: number
partOf?: string
items: Activity[]
}

View file

@ -0,0 +1,9 @@
import { Activity } from './activity'
export interface ActivityPubOrderedCollection {
'@context': string[]
type: 'OrderedCollection' | 'OrderedCollectionPage'
totalItems: number
partOf?: string
orderedItems: Activity[]
}

View file

@ -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

View file

@ -0,0 +1,6 @@
export interface ActivityPubSignature {
type: 'GraphSignature2012'
created: Date,
creator: string
signatureValue: string
}

View file

@ -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'

View file

@ -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
}

View file

@ -0,0 +1,3 @@
export * from './common-objects'
export * from './video-channel-object'
export * from './video-torrent-object'

View file

@ -0,0 +1,8 @@
import { ActivityIdentifierObject } from './common-objects'
export interface VideoChannelObject {
type: 'VideoChannel'
name: string
content: string
uuid: ActivityIdentifierObject
}

View file

@ -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[]
}

View file

@ -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
}[]
}

View file

@ -1,3 +1,4 @@
export * from './activitypub'
export * from './pods'
export * from './users'
export * from './videos'

View file

@ -1 +1,2 @@
export type JobState = 'pending' | 'processing' | 'error' | 'success'
export type JobCategory = 'transcoding' | 'http-request'

View file

@ -13,7 +13,7 @@ export interface VideoFile {
export interface Video {
id: number
uuid: string
author: string
account: string
createdAt: Date | string
updatedAt: Date | string
categoryLabel: string

183
yarn.lock
View file

@ -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"