mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-08-17 17:30:32 +03:00
Begin activitypub
This commit is contained in:
parent
343ad675f2
commit
e4f97babf7
@ -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",
|
||||
|
65
server/controllers/activitypub/client.ts
Normal file
65
server/controllers/activitypub/client.ts
Normal 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)
|
||||
}
|
72
server/controllers/activitypub/inbox.ts
Normal file
72
server/controllers/activitypub/inbox.ts
Normal 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)
|
||||
}
|
||||
}
|
15
server/controllers/activitypub/index.ts
Normal file
15
server/controllers/activitypub/index.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
123
server/helpers/activitypub.ts
Normal file
123
server/helpers/activitypub.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
123
server/helpers/custom-validators/activitypub/account.ts
Normal file
123
server/helpers/custom-validators/activitypub/account.ts
Normal 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
|
||||
}
|
4
server/helpers/custom-validators/activitypub/index.ts
Normal file
4
server/helpers/custom-validators/activitypub/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './account'
|
||||
export * from './signature'
|
||||
export * from './misc'
|
||||
export * from './videos'
|
17
server/helpers/custom-validators/activitypub/misc.ts
Normal file
17
server/helpers/custom-validators/activitypub/misc.ts
Normal 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
|
||||
}
|
22
server/helpers/custom-validators/activitypub/signature.ts
Normal file
22
server/helpers/custom-validators/activitypub/signature.ts
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export * from './remote'
|
||||
export * from './activitypub'
|
||||
export * from './misc'
|
||||
export * from './pods'
|
||||
export * from './pods'
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './videos'
|
@ -1,4 +1,3 @@
|
||||
import * as Promise from 'bluebird'
|
||||
import * as ffmpeg from 'fluent-ffmpeg'
|
||||
|
||||
import { CONFIG } from '../initializers'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
44
server/helpers/webfinger.ts
Normal file
44
server/helpers/webfinger.ts
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
@ -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 () {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
3
server/lib/activitypub/index.ts
Normal file
3
server/lib/activitypub/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './process-create'
|
||||
export * from './process-flag'
|
||||
export * from './process-update'
|
104
server/lib/activitypub/process-create.ts
Normal file
104
server/lib/activitypub/process-create.ts
Normal 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) {
|
||||
|
||||
}
|
17
server/lib/activitypub/process-flag.ts
Normal file
17
server/lib/activitypub/process-flag.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {
|
||||
ActivityCreate,
|
||||
VideoTorrentObject,
|
||||
VideoChannelObject
|
||||
} from '../../../shared'
|
||||
|
||||
function processFlagActivity (activity: ActivityCreate) {
|
||||
// empty
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
processFlagActivity
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
29
server/lib/activitypub/process-update.ts
Normal file
29
server/lib/activitypub/process-update.ts
Normal 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) {
|
||||
|
||||
}
|
129
server/lib/activitypub/send-request.ts
Normal file
129
server/lib/activitypub/send-request.ts
Normal 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)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './activitypub'
|
||||
export * from './cache'
|
||||
export * from './jobs'
|
||||
export * from './request'
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
1
server/lib/jobs/http-request-job-scheduler/index.ts
Normal file
1
server/lib/jobs/http-request-job-scheduler/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './http-request-job-scheduler'
|
@ -1 +1,2 @@
|
||||
export * from './job-scheduler'
|
||||
export * from './http-request-job-scheduler'
|
||||
export * from './transcoding-job-scheduler'
|
||||
|
@ -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)
|
||||
|
1
server/lib/jobs/transcoding-job-scheduler/index.ts
Normal file
1
server/lib/jobs/transcoding-job-scheduler/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './transcoding-job-scheduler'
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
57
server/middlewares/activitypub.ts
Normal file
57
server/middlewares/activitypub.ts
Normal 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
|
||||
}
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
53
server/middlewares/validators/account.ts
Normal file
53
server/middlewares/validators/account.ts
Normal 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)
|
||||
})
|
||||
}
|
30
server/middlewares/validators/activitypub/signature.ts
Normal file
30
server/middlewares/validators/activitypub/signature.ts
Normal 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
|
||||
}
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
23
server/models/account/account-follow-interface.ts
Normal file
23
server/models/account/account-follow-interface.ts
Normal 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> {}
|
56
server/models/account/account-follow.ts
Normal file
56
server/models/account/account-follow.ts
Normal 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'
|
||||
})
|
||||
}
|
74
server/models/account/account-interface.ts
Normal file
74
server/models/account/account-interface.ts
Normal 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> {}
|
26
server/models/account/account-video-rate-interface.ts
Normal file
26
server/models/account/account-video-rate-interface.ts
Normal 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> {}
|
@ -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)
|
||||
}
|
444
server/models/account/account.ts
Normal file
444
server/models/account/account.ts
Normal 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)
|
||||
}
|
||||
}
|
4
server/models/account/index.ts
Normal file
4
server/models/account/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './account-interface'
|
||||
export * from './account-follow-interface'
|
||||
export * from './account-video-rate-interface'
|
||||
export * from './user-interface'
|
@ -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> {
|
@ -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 = {
|
@ -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'
|
||||
|
@ -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 {
|
||||
|
@ -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' ]
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
export * from './user-video-rate-interface'
|
||||
export * from './user-interface'
|
@ -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> {}
|
@ -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> {}
|
@ -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)
|
||||
}
|
@ -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> {}
|
||||
|
@ -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
|
||||
|
@ -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> {}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
34
shared/models/activitypub/activity.ts
Normal file
34
shared/models/activitypub/activity.ts
Normal 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
|
||||
}
|
27
shared/models/activitypub/activitypub-actor.ts
Normal file
27
shared/models/activitypub/activitypub-actor.ts
Normal 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
|
||||
}
|
9
shared/models/activitypub/activitypub-collection.ts
Normal file
9
shared/models/activitypub/activitypub-collection.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Activity } from './activity'
|
||||
|
||||
export interface ActivityPubCollection {
|
||||
'@context': string[]
|
||||
type: 'Collection' | 'CollectionPage'
|
||||
totalItems: number
|
||||
partOf?: string
|
||||
items: Activity[]
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Activity } from './activity'
|
||||
|
||||
export interface ActivityPubOrderedCollection {
|
||||
'@context': string[]
|
||||
type: 'OrderedCollection' | 'OrderedCollectionPage'
|
||||
totalItems: number
|
||||
partOf?: string
|
||||
orderedItems: Activity[]
|
||||
}
|
5
shared/models/activitypub/activitypub-root.ts
Normal file
5
shared/models/activitypub/activitypub-root.ts
Normal 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
|
6
shared/models/activitypub/activitypub-signature.ts
Normal file
6
shared/models/activitypub/activitypub-signature.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ActivityPubSignature {
|
||||
type: 'GraphSignature2012'
|
||||
created: Date,
|
||||
creator: string
|
||||
signatureValue: string
|
||||
}
|
8
shared/models/activitypub/index.ts
Normal file
8
shared/models/activitypub/index.ts
Normal 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'
|
25
shared/models/activitypub/objects/common-objects.ts
Normal file
25
shared/models/activitypub/objects/common-objects.ts
Normal 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
|
||||
}
|
3
shared/models/activitypub/objects/index.ts
Normal file
3
shared/models/activitypub/objects/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './common-objects'
|
||||
export * from './video-channel-object'
|
||||
export * from './video-torrent-object'
|
@ -0,0 +1,8 @@
|
||||
import { ActivityIdentifierObject } from './common-objects'
|
||||
|
||||
export interface VideoChannelObject {
|
||||
type: 'VideoChannel'
|
||||
name: string
|
||||
content: string
|
||||
uuid: ActivityIdentifierObject
|
||||
}
|
25
shared/models/activitypub/objects/video-torrent-object.ts
Normal file
25
shared/models/activitypub/objects/video-torrent-object.ts
Normal 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[]
|
||||
}
|
9
shared/models/activitypub/webfinger.ts
Normal file
9
shared/models/activitypub/webfinger.ts
Normal 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
|
||||
}[]
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './activitypub'
|
||||
export * from './pods'
|
||||
export * from './users'
|
||||
export * from './videos'
|
||||
|
@ -1 +1,2 @@
|
||||
export type JobState = 'pending' | 'processing' | 'error' | 'success'
|
||||
export type JobCategory = 'transcoding' | 'http-request'
|
||||
|
@ -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
183
yarn.lock
@ -125,6 +125,10 @@
|
||||
"@types/node" "*"
|
||||
"@types/parse-torrent-file" "*"
|
||||
|
||||
"@types/pem@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.3.tgz#0c864c8b79e43fef6367db895f60fd1edd10e86c"
|
||||
|
||||
"@types/request@^2.0.3":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.0.7.tgz#a2aa5a57317c21971d9b024e393091ab2c99ab98"
|
||||
@ -456,6 +460,23 @@ bindings@~1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
|
||||
|
||||
bitcore-lib@^0.13.7:
|
||||
version "0.13.19"
|
||||
resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc"
|
||||
dependencies:
|
||||
bn.js "=2.0.4"
|
||||
bs58 "=2.0.0"
|
||||
buffer-compare "=1.0.0"
|
||||
elliptic "=3.0.3"
|
||||
inherits "=2.0.1"
|
||||
lodash "=3.10.1"
|
||||
|
||||
"bitcore-message@github:CoMakery/bitcore-message#dist":
|
||||
version "1.0.2"
|
||||
resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
|
||||
dependencies:
|
||||
bitcore-lib "^0.13.7"
|
||||
|
||||
bitfield@^1.0.1, bitfield@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-1.1.2.tgz#a5477f00e33f2a76edc209aaf26bf09394a378cf"
|
||||
@ -558,6 +579,14 @@ bluebird@^3.0.5, bluebird@^3.4.6, bluebird@^3.5.0:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
bn.js@=2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
|
||||
|
||||
bn.js@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
|
||||
|
||||
bn.js@^4.4.0:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
@ -622,6 +651,10 @@ braces@^1.8.2:
|
||||
preserve "^0.2.0"
|
||||
repeat-element "^1.1.2"
|
||||
|
||||
brorand@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
|
||||
browser-stdout@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
|
||||
@ -630,10 +663,18 @@ browserify-package-json@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
|
||||
|
||||
bs58@=2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
|
||||
|
||||
buffer-alloc-unsafe@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.0.0.tgz#474aa88f34e7bc75fa311d2e6457409c5846c3fe"
|
||||
|
||||
buffer-compare@=1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
|
||||
|
||||
buffer-equals@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5"
|
||||
@ -726,6 +767,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
|
||||
check-error@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
@ -833,6 +878,12 @@ commander@2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d"
|
||||
|
||||
commander@~2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
|
||||
dependencies:
|
||||
graceful-readlink ">= 1.0.0"
|
||||
|
||||
compact2string@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/compact2string/-/compact2string-1.4.0.tgz#a99cd96ea000525684b269683ae2222d6eea7b49"
|
||||
@ -958,6 +1009,10 @@ cross-spawn@^5.0.1:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
crypt@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||
|
||||
cryptiles@2.x.x:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||
@ -1148,6 +1203,15 @@ ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
|
||||
elliptic@=3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
|
||||
dependencies:
|
||||
bn.js "^2.0.0"
|
||||
brorand "^1.0.1"
|
||||
hash.js "^1.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
encodeurl@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
|
||||
@ -1208,10 +1272,22 @@ es6-map@^0.1.3:
|
||||
es6-symbol "~3.1.1"
|
||||
event-emitter "~0.3.5"
|
||||
|
||||
es6-promise@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.3.0.tgz#96edb9f2fdb01995822b263dd8aadab6748181bc"
|
||||
|
||||
es6-promise@^3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
|
||||
|
||||
es6-promise@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-2.0.1.tgz#ccc4963e679f0ca9fb187c777b9e583d3c7573c2"
|
||||
|
||||
es6-promise@~4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
|
||||
|
||||
es6-set@~0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
|
||||
@ -1834,6 +1910,10 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2:
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||
|
||||
"graceful-readlink@>= 1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
growl@1.10.3:
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
|
||||
@ -1890,6 +1970,13 @@ has@^1.0.1:
|
||||
dependencies:
|
||||
function-bind "^1.0.2"
|
||||
|
||||
hash.js@^1.0.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
hawk@3.1.3, hawk@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
|
||||
@ -1990,6 +2077,10 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, i
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
|
||||
inherits@=2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
|
||||
|
||||
ini@^1.3.4, ini@~1.3.0:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
|
||||
@ -2052,7 +2143,7 @@ is-bluebird@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-bluebird/-/is-bluebird-1.0.2.tgz#096439060f4aa411abee19143a84d6a55346d6e2"
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
is-buffer@^1.1.5, is-buffer@~1.1.1:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
||||
@ -2269,6 +2360,35 @@ jsonify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
|
||||
jsonld-signatures@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-1.2.1.tgz#493df5df9cd3a9f1b1cb296bbd3d081679f20ca8"
|
||||
dependencies:
|
||||
async "^1.5.2"
|
||||
bitcore-message "github:CoMakery/bitcore-message#dist"
|
||||
commander "~2.9.0"
|
||||
es6-promise "~4.0.5"
|
||||
jsonld "0.4.3"
|
||||
node-forge "~0.6.45"
|
||||
|
||||
jsonld@0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.3.tgz#0bbc929190064d6650a5af5876e1bfdf0ed288f3"
|
||||
dependencies:
|
||||
es6-promise "~2.0.1"
|
||||
pkginfo "~0.3.0"
|
||||
request "^2.61.0"
|
||||
xmldom "0.1.19"
|
||||
|
||||
jsonld@^0.4.12:
|
||||
version "0.4.12"
|
||||
resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.4.12.tgz#a02f205d5341414df1b6d8414f1b967a712073e8"
|
||||
dependencies:
|
||||
es6-promise "^2.0.0"
|
||||
pkginfo "~0.4.0"
|
||||
request "^2.61.0"
|
||||
xmldom "0.1.19"
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
|
||||
@ -2439,6 +2559,10 @@ lodash@4.17.4, lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.16.0, lo
|
||||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
|
||||
|
||||
lodash@=3.10.1:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
|
||||
lowercase-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
@ -2479,6 +2603,14 @@ map-stream@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||
|
||||
md5@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
|
||||
dependencies:
|
||||
charenc "~0.0.1"
|
||||
crypt "~0.0.1"
|
||||
is-buffer "~1.1.1"
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
@ -2539,6 +2671,10 @@ mimic-response@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
|
||||
|
||||
minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
@ -2667,6 +2803,10 @@ node-abi@^2.1.1:
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-forge@~0.6.45:
|
||||
version "0.6.49"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.49.tgz#f1ee95d5d74623938fe19d698aa5a26d54d2f60f"
|
||||
|
||||
node-pre-gyp@0.6.36:
|
||||
version "0.6.36"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
|
||||
@ -2820,10 +2960,6 @@ onetime@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
|
||||
|
||||
openssl-wrapper@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz#c01ec98e4dcd2b5dfe0b693f31827200e3b81b07"
|
||||
|
||||
optionator@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||
@ -2839,7 +2975,7 @@ os-homedir@1.0.2, os-homedir@^1.0.0, os-homedir@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
|
||||
@ -2970,6 +3106,15 @@ pause-stream@0.0.11:
|
||||
dependencies:
|
||||
through "~2.3"
|
||||
|
||||
pem@^1.12.3:
|
||||
version "1.12.3"
|
||||
resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.3.tgz#b1fb5c8b79da8d18146c27fee79b0d4ddf9905b3"
|
||||
dependencies:
|
||||
md5 "^2.2.1"
|
||||
os-tmpdir "^1.0.1"
|
||||
safe-buffer "^5.1.1"
|
||||
which "^1.2.4"
|
||||
|
||||
performance-now@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
||||
@ -3074,6 +3219,14 @@ pkg-up@^1.0.0:
|
||||
dependencies:
|
||||
find-up "^1.0.0"
|
||||
|
||||
pkginfo@~0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
|
||||
|
||||
pkginfo@~0.4.0:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
|
||||
|
||||
pluralize@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
|
||||
@ -3353,7 +3506,7 @@ request@2.81.0:
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.0.0"
|
||||
|
||||
request@^2.81.0:
|
||||
request@^2.61.0, request@^2.81.0:
|
||||
version "2.83.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
|
||||
dependencies:
|
||||
@ -4255,6 +4408,12 @@ videostream@^2.3.0:
|
||||
pump "^1.0.1"
|
||||
range-slice-stream "^1.2.0"
|
||||
|
||||
webfinger.js@^2.6.6:
|
||||
version "2.6.6"
|
||||
resolved "https://registry.yarnpkg.com/webfinger.js/-/webfinger.js-2.6.6.tgz#52ebdc85da8c8fb6beb690e8e32594c99d2ff4ae"
|
||||
dependencies:
|
||||
xhr2 "^0.1.4"
|
||||
|
||||
webtorrent@^0.98.0:
|
||||
version "0.98.20"
|
||||
resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.98.20.tgz#f335869185a64447b6fe730c3c66265620b8c14a"
|
||||
@ -4302,7 +4461,7 @@ webtorrent@^0.98.0:
|
||||
xtend "^4.0.1"
|
||||
zero-fill "^2.2.3"
|
||||
|
||||
which@^1.1.1, which@^1.2.9:
|
||||
which@^1.1.1, which@^1.2.4, which@^1.2.9:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
|
||||
dependencies:
|
||||
@ -4378,6 +4537,14 @@ xdg-basedir@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
|
||||
|
||||
xhr2@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
|
||||
|
||||
xmldom@0.1.19:
|
||||
version "0.1.19"
|
||||
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
|
||||
|
||||
xtend@4.0.1, xtend@^4.0.0, xtend@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
Loading…
Reference in New Issue
Block a user