aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-11-09 17:51:58 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-11-27 19:40:51 +0100
commite4f97babf701481b55cc10fb3448feab5f97c867 (patch)
treeaf37402a594dc5ff09f71ecb0687e8cfe4cdb471 /server
parent343ad675f2a26c15b86150a9a3552e619d5d44f4 (diff)
downloadPeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.tar.gz
PeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.tar.zst
PeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.zip
Begin activitypub
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts65
-rw-r--r--server/controllers/activitypub/inbox.ts72
-rw-r--r--server/controllers/activitypub/index.ts15
-rw-r--r--server/controllers/activitypub/pods.ts (renamed from server/controllers/api/remote/pods.ts)0
-rw-r--r--server/controllers/activitypub/videos.ts (renamed from server/controllers/api/remote/videos.ts)0
-rw-r--r--server/controllers/api/remote/index.ts18
-rw-r--r--server/helpers/activitypub.ts123
-rw-r--r--server/helpers/core-utils.ts26
-rw-r--r--server/helpers/custom-validators/activitypub/account.ts123
-rw-r--r--server/helpers/custom-validators/activitypub/index.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts17
-rw-r--r--server/helpers/custom-validators/activitypub/signature.ts22
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts (renamed from server/helpers/custom-validators/remote/videos.ts)0
-rw-r--r--server/helpers/custom-validators/index.ts2
-rw-r--r--server/helpers/custom-validators/remote/index.ts1
-rw-r--r--server/helpers/ffmpeg-utils.ts1
-rw-r--r--server/helpers/index.ts2
-rw-r--r--server/helpers/peertube-crypto.ts158
-rw-r--r--server/helpers/requests.ts78
-rw-r--r--server/helpers/webfinger.ts44
-rw-r--r--server/initializers/checker.ts2
-rw-r--r--server/initializers/constants.ts31
-rw-r--r--server/initializers/database.ts14
-rw-r--r--server/lib/activitypub/index.ts3
-rw-r--r--server/lib/activitypub/process-create.ts104
-rw-r--r--server/lib/activitypub/process-flag.ts17
-rw-r--r--server/lib/activitypub/process-update.ts29
-rw-r--r--server/lib/activitypub/send-request.ts129
-rw-r--r--server/lib/index.ts1
-rw-r--r--server/lib/jobs/handlers/index.ts17
-rw-r--r--server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts25
-rw-r--r--server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts17
-rw-r--r--server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts25
-rw-r--r--server/lib/jobs/http-request-job-scheduler/index.ts1
-rw-r--r--server/lib/jobs/index.ts3
-rw-r--r--server/lib/jobs/job-scheduler.ts35
-rw-r--r--server/lib/jobs/transcoding-job-scheduler/index.ts1
-rw-r--r--server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts17
-rw-r--r--server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts (renamed from server/lib/jobs/handlers/video-file-optimizer.ts)0
-rw-r--r--server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts (renamed from server/lib/jobs/handlers/video-file-transcoder.ts)0
-rw-r--r--server/lib/user.ts18
-rw-r--r--server/lib/video-channel.ts10
-rw-r--r--server/middlewares/activitypub.ts57
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/secure.ts55
-rw-r--r--server/middlewares/validators/account.ts53
-rw-r--r--server/middlewares/validators/activitypub/index.ts (renamed from server/middlewares/validators/remote/index.ts)0
-rw-r--r--server/middlewares/validators/activitypub/pods.ts (renamed from server/middlewares/validators/remote/pods.ts)0
-rw-r--r--server/middlewares/validators/activitypub/signature.ts30
-rw-r--r--server/middlewares/validators/activitypub/videos.ts (renamed from server/middlewares/validators/remote/videos.ts)0
-rw-r--r--server/middlewares/validators/index.ts3
-rw-r--r--server/middlewares/validators/remote/signature.ts22
-rw-r--r--server/models/account/account-follow-interface.ts23
-rw-r--r--server/models/account/account-follow.ts56
-rw-r--r--server/models/account/account-interface.ts74
-rw-r--r--server/models/account/account-video-rate-interface.ts26
-rw-r--r--server/models/account/account-video-rate.ts (renamed from server/models/user/user-video-rate.ts)36
-rw-r--r--server/models/account/account.ts444
-rw-r--r--server/models/account/index.ts4
-rw-r--r--server/models/account/user-interface.ts (renamed from server/models/user/user-interface.ts)20
-rw-r--r--server/models/account/user.ts (renamed from server/models/user/user.ts)27
-rw-r--r--server/models/index.ts2
-rw-r--r--server/models/job/job-interface.ts6
-rw-r--r--server/models/job/job.ts12
-rw-r--r--server/models/oauth/oauth-token-interface.ts2
-rw-r--r--server/models/pod/pod-interface.ts2
-rw-r--r--server/models/pod/pod.ts12
-rw-r--r--server/models/user/index.ts2
-rw-r--r--server/models/user/user-video-rate-interface.ts26
-rw-r--r--server/models/video/author-interface.ts45
-rw-r--r--server/models/video/author.ts171
-rw-r--r--server/models/video/video-channel-interface.ts38
-rw-r--r--server/models/video/video-channel.ts100
-rw-r--r--server/models/video/video-interface.ts60
-rw-r--r--server/models/video/video.ts378
75 files changed, 2152 insertions, 906 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
new file mode 100644
index 000000000..28d08b3f4
--- /dev/null
+++ b/server/controllers/activitypub/client.ts
@@ -0,0 +1,65 @@
1// Intercept ActivityPub client requests
2import * as express from 'express'
3
4import { database as db } from '../../initializers'
5import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
6import { pageToStartAndCount } from '../../helpers'
7import { AccountInstance } from '../../models'
8import { activityPubCollectionPagination } from '../../helpers/activitypub'
9import { ACTIVITY_PUB } from '../../initializers/constants'
10import { asyncMiddleware } from '../../middlewares/async'
11
12const activityPubClientRouter = express.Router()
13
14activityPubClientRouter.get('/account/:name',
15 executeIfActivityPub(localAccountValidator),
16 executeIfActivityPub(asyncMiddleware(accountController))
17)
18
19activityPubClientRouter.get('/account/:name/followers',
20 executeIfActivityPub(localAccountValidator),
21 executeIfActivityPub(asyncMiddleware(accountFollowersController))
22)
23
24activityPubClientRouter.get('/account/:name/following',
25 executeIfActivityPub(localAccountValidator),
26 executeIfActivityPub(asyncMiddleware(accountFollowingController))
27)
28
29// ---------------------------------------------------------------------------
30
31export {
32 activityPubClientRouter
33}
34
35// ---------------------------------------------------------------------------
36
37async function accountController (req: express.Request, res: express.Response, next: express.NextFunction) {
38 const account: AccountInstance = res.locals.account
39
40 return res.json(account.toActivityPubObject()).end()
41}
42
43async function accountFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) {
44 const account: AccountInstance = res.locals.account
45
46 const page = req.params.page || 1
47 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
48
49 const result = await db.Account.listFollowerUrlsForApi(account.name, start, count)
50 const activityPubResult = activityPubCollectionPagination(req.url, page, result)
51
52 return res.json(activityPubResult)
53}
54
55async function accountFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) {
56 const account: AccountInstance = res.locals.account
57
58 const page = req.params.page || 1
59 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
60
61 const result = await db.Account.listFollowingUrlsForApi(account.name, start, count)
62 const activityPubResult = activityPubCollectionPagination(req.url, page, result)
63
64 return res.json(activityPubResult)
65}
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
new file mode 100644
index 000000000..79d989c2c
--- /dev/null
+++ b/server/controllers/activitypub/inbox.ts
@@ -0,0 +1,72 @@
1import * as express from 'express'
2
3import {
4 processCreateActivity,
5 processUpdateActivity,
6 processFlagActivity
7} from '../../lib'
8import {
9 Activity,
10 ActivityType,
11 RootActivity,
12 ActivityPubCollection,
13 ActivityPubOrderedCollection
14} from '../../../shared'
15import {
16 signatureValidator,
17 checkSignature,
18 asyncMiddleware
19} from '../../middlewares'
20import { logger } from '../../helpers'
21
22const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
23 Create: processCreateActivity,
24 Update: processUpdateActivity,
25 Flag: processFlagActivity
26}
27
28const inboxRouter = express.Router()
29
30inboxRouter.post('/',
31 signatureValidator,
32 asyncMiddleware(checkSignature),
33 // inboxValidator,
34 asyncMiddleware(inboxController)
35)
36
37// ---------------------------------------------------------------------------
38
39export {
40 inboxRouter
41}
42
43// ---------------------------------------------------------------------------
44
45async function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
46 const rootActivity: RootActivity = req.body
47 let activities: Activity[] = []
48
49 if ([ 'Collection', 'CollectionPage' ].indexOf(rootActivity.type) !== -1) {
50 activities = (rootActivity as ActivityPubCollection).items
51 } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].indexOf(rootActivity.type) !== -1) {
52 activities = (rootActivity as ActivityPubOrderedCollection).orderedItems
53 } else {
54 activities = [ rootActivity as Activity ]
55 }
56
57 await processActivities(activities)
58
59 res.status(204).end()
60}
61
62async function processActivities (activities: Activity[]) {
63 for (const activity of activities) {
64 const activityProcessor = processActivity[activity.type]
65 if (activityProcessor === undefined) {
66 logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id })
67 continue
68 }
69
70 await activityProcessor(activity)
71 }
72}
diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts
new file mode 100644
index 000000000..7a4602b37
--- /dev/null
+++ b/server/controllers/activitypub/index.ts
@@ -0,0 +1,15 @@
1import * as express from 'express'
2
3import { badRequest } from '../../helpers'
4import { inboxRouter } from './inbox'
5
6const remoteRouter = express.Router()
7
8remoteRouter.use('/inbox', inboxRouter)
9remoteRouter.use('/*', badRequest)
10
11// ---------------------------------------------------------------------------
12
13export {
14 remoteRouter
15}
diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/activitypub/pods.ts
index 326eb61ac..326eb61ac 100644
--- a/server/controllers/api/remote/pods.ts
+++ b/server/controllers/activitypub/pods.ts
diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/activitypub/videos.ts
index cba47f0a1..cba47f0a1 100644
--- a/server/controllers/api/remote/videos.ts
+++ b/server/controllers/activitypub/videos.ts
diff --git a/server/controllers/api/remote/index.ts b/server/controllers/api/remote/index.ts
deleted file mode 100644
index d3522772b..000000000
--- a/server/controllers/api/remote/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import * as express from 'express'
2
3import { badRequest } from '../../../helpers'
4
5import { remotePodsRouter } from './pods'
6import { remoteVideosRouter } from './videos'
7
8const remoteRouter = express.Router()
9
10remoteRouter.use('/pods', remotePodsRouter)
11remoteRouter.use('/videos', remoteVideosRouter)
12remoteRouter.use('/*', badRequest)
13
14// ---------------------------------------------------------------------------
15
16export {
17 remoteRouter
18}
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
new file mode 100644
index 000000000..ecb509b66
--- /dev/null
+++ b/server/helpers/activitypub.ts
@@ -0,0 +1,123 @@
1import * as url from 'url'
2
3import { database as db } from '../initializers'
4import { logger } from './logger'
5import { doRequest } from './requests'
6import { isRemoteAccountValid } from './custom-validators'
7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
8import { ResultList } from '../../shared/models/result-list.model'
9
10async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
11 const options = {
12 uri: accountUrl,
13 method: 'GET'
14 }
15
16 let requestResult
17 try {
18 requestResult = await doRequest(options)
19 } catch (err) {
20 logger.warning('Cannot fetch remote account %s.', accountUrl, err)
21 return undefined
22 }
23
24 const accountJSON: ActivityPubActor = requestResult.body
25 if (isRemoteAccountValid(accountJSON) === false) return undefined
26
27 const followersCount = await fetchAccountCount(accountJSON.followers)
28 const followingCount = await fetchAccountCount(accountJSON.following)
29
30 const account = db.Account.build({
31 uuid: accountJSON.uuid,
32 name: accountJSON.preferredUsername,
33 url: accountJSON.url,
34 publicKey: accountJSON.publicKey.publicKeyPem,
35 privateKey: null,
36 followersCount: followersCount,
37 followingCount: followingCount,
38 inboxUrl: accountJSON.inbox,
39 outboxUrl: accountJSON.outbox,
40 sharedInboxUrl: accountJSON.endpoints.sharedInbox,
41 followersUrl: accountJSON.followers,
42 followingUrl: accountJSON.following
43 })
44
45 const accountHost = url.parse(account.url).host
46 const podOptions = {
47 where: {
48 host: accountHost
49 },
50 defaults: {
51 host: accountHost
52 }
53 }
54 const pod = await db.Pod.findOrCreate(podOptions)
55
56 return { account, pod }
57}
58
59function activityPubContextify (data: object) {
60 return Object.assign(data,{
61 '@context': [
62 'https://www.w3.org/ns/activitystreams',
63 'https://w3id.org/security/v1',
64 {
65 'Hashtag': 'as:Hashtag',
66 'uuid': 'http://schema.org/identifier',
67 'category': 'http://schema.org/category',
68 'licence': 'http://schema.org/license',
69 'nsfw': 'as:sensitive',
70 'language': 'http://schema.org/inLanguage',
71 'views': 'http://schema.org/Number',
72 'size': 'http://schema.org/Number'
73 }
74 ]
75 })
76}
77
78function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
79 const baseUrl = url.split('?').shift
80
81 const obj = {
82 id: baseUrl,
83 type: 'Collection',
84 totalItems: result.total,
85 first: {
86 id: baseUrl + '?page=' + page,
87 type: 'CollectionPage',
88 totalItems: result.total,
89 next: baseUrl + '?page=' + (page + 1),
90 partOf: baseUrl,
91 items: result.data
92 }
93 }
94
95 return activityPubContextify(obj)
96}
97
98// ---------------------------------------------------------------------------
99
100export {
101 fetchRemoteAccountAndCreatePod,
102 activityPubContextify,
103 activityPubCollectionPagination
104}
105
106// ---------------------------------------------------------------------------
107
108async function fetchAccountCount (url: string) {
109 const options = {
110 uri: url,
111 method: 'GET'
112 }
113
114 let requestResult
115 try {
116 requestResult = await doRequest(options)
117 } catch (err) {
118 logger.warning('Cannot fetch remote account count %s.', url, err)
119 return undefined
120 }
121
122 return requestResult.totalItems ? requestResult.totalItems : 0
123}
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 3dae78144..d8748e1d7 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -19,8 +19,10 @@ import * as mkdirp from 'mkdirp'
19import * as bcrypt from 'bcrypt' 19import * as bcrypt from 'bcrypt'
20import * as createTorrent from 'create-torrent' 20import * as createTorrent from 'create-torrent'
21import * as rimraf from 'rimraf' 21import * as rimraf from 'rimraf'
22import * as openssl from 'openssl-wrapper' 22import * as pem from 'pem'
23import * as Promise from 'bluebird' 23import * as jsonld from 'jsonld'
24import * as jsig from 'jsonld-signatures'
25jsig.use('jsonld', jsonld)
24 26
25function isTestInstance () { 27function isTestInstance () {
26 return process.env.NODE_ENV === 'test' 28 return process.env.NODE_ENV === 'test'
@@ -54,6 +56,12 @@ function escapeHTML (stringParam) {
54 return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s]) 56 return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s])
55} 57}
56 58
59function pageToStartAndCount (page: number, itemsPerPage: number) {
60 const start = (page - 1) * itemsPerPage
61
62 return { start, count: itemsPerPage }
63}
64
57function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { 65function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
58 return function promisified (): Promise<A> { 66 return function promisified (): Promise<A> {
59 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { 67 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
@@ -104,13 +112,16 @@ const readdirPromise = promisify1<string, string[]>(readdir)
104const mkdirpPromise = promisify1<string, string>(mkdirp) 112const mkdirpPromise = promisify1<string, string>(mkdirp)
105const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes) 113const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
106const accessPromise = promisify1WithVoid<string | Buffer>(access) 114const accessPromise = promisify1WithVoid<string | Buffer>(access)
107const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec) 115const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
116const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
108const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) 117const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
109const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) 118const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
110const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash) 119const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
111const createTorrentPromise = promisify2<string, any, any>(createTorrent) 120const createTorrentPromise = promisify2<string, any, any>(createTorrent)
112const rimrafPromise = promisify1WithVoid<string>(rimraf) 121const rimrafPromise = promisify1WithVoid<string>(rimraf)
113const statPromise = promisify1<string, Stats>(stat) 122const statPromise = promisify1<string, Stats>(stat)
123const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign)
124const jsonldVerifyPromise = promisify2<object, object, object>(jsig.verify)
114 125
115// --------------------------------------------------------------------------- 126// ---------------------------------------------------------------------------
116 127
@@ -118,9 +129,11 @@ export {
118 isTestInstance, 129 isTestInstance,
119 root, 130 root,
120 escapeHTML, 131 escapeHTML,
132 pageToStartAndCount,
121 133
122 promisify0, 134 promisify0,
123 promisify1, 135 promisify1,
136
124 readdirPromise, 137 readdirPromise,
125 readFilePromise, 138 readFilePromise,
126 readFileBufferPromise, 139 readFileBufferPromise,
@@ -130,11 +143,14 @@ export {
130 mkdirpPromise, 143 mkdirpPromise,
131 pseudoRandomBytesPromise, 144 pseudoRandomBytesPromise,
132 accessPromise, 145 accessPromise,
133 opensslExecPromise, 146 createPrivateKey,
147 getPublicKey,
134 bcryptComparePromise, 148 bcryptComparePromise,
135 bcryptGenSaltPromise, 149 bcryptGenSaltPromise,
136 bcryptHashPromise, 150 bcryptHashPromise,
137 createTorrentPromise, 151 createTorrentPromise,
138 rimrafPromise, 152 rimrafPromise,
139 statPromise 153 statPromise,
154 jsonldSignPromise,
155 jsonldVerifyPromise
140} 156}
diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts
new file mode 100644
index 000000000..8a7d1b7fe
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/account.ts
@@ -0,0 +1,123 @@
1import * as validator from 'validator'
2
3import { exists, isUUIDValid } from '../misc'
4import { isActivityPubUrlValid } from './misc'
5import { isUserUsernameValid } from '../users'
6
7function isAccountEndpointsObjectValid (endpointObject: any) {
8 return isAccountSharedInboxValid(endpointObject.sharedInbox)
9}
10
11function isAccountSharedInboxValid (sharedInbox: string) {
12 return isActivityPubUrlValid(sharedInbox)
13}
14
15function isAccountPublicKeyObjectValid (publicKeyObject: any) {
16 return isAccountPublicKeyIdValid(publicKeyObject.id) &&
17 isAccountPublicKeyOwnerValid(publicKeyObject.owner) &&
18 isAccountPublicKeyValid(publicKeyObject.publicKeyPem)
19}
20
21function isAccountPublicKeyIdValid (id: string) {
22 return isActivityPubUrlValid(id)
23}
24
25function isAccountTypeValid (type: string) {
26 return type === 'Person' || type === 'Application'
27}
28
29function isAccountPublicKeyOwnerValid (owner: string) {
30 return isActivityPubUrlValid(owner)
31}
32
33function isAccountPublicKeyValid (publicKey: string) {
34 return exists(publicKey) &&
35 typeof publicKey === 'string' &&
36 publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
37 publicKey.endsWith('-----END PUBLIC KEY-----')
38}
39
40function isAccountIdValid (id: string) {
41 return isActivityPubUrlValid(id)
42}
43
44function isAccountFollowingValid (id: string) {
45 return isActivityPubUrlValid(id)
46}
47
48function isAccountFollowersValid (id: string) {
49 return isActivityPubUrlValid(id)
50}
51
52function isAccountInboxValid (inbox: string) {
53 return isActivityPubUrlValid(inbox)
54}
55
56function isAccountOutboxValid (outbox: string) {
57 return isActivityPubUrlValid(outbox)
58}
59
60function isAccountNameValid (name: string) {
61 return isUserUsernameValid(name)
62}
63
64function isAccountPreferredUsernameValid (preferredUsername: string) {
65 return isAccountNameValid(preferredUsername)
66}
67
68function isAccountUrlValid (url: string) {
69 return isActivityPubUrlValid(url)
70}
71
72function isAccountPrivateKeyValid (privateKey: string) {
73 return exists(privateKey) &&
74 typeof privateKey === 'string' &&
75 privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
76 privateKey.endsWith('-----END RSA PRIVATE KEY-----')
77}
78
79function isRemoteAccountValid (remoteAccount: any) {
80 return isAccountIdValid(remoteAccount.id) &&
81 isUUIDValid(remoteAccount.uuid) &&
82 isAccountTypeValid(remoteAccount.type) &&
83 isAccountFollowingValid(remoteAccount.following) &&
84 isAccountFollowersValid(remoteAccount.followers) &&
85 isAccountInboxValid(remoteAccount.inbox) &&
86 isAccountOutboxValid(remoteAccount.outbox) &&
87 isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
88 isAccountUrlValid(remoteAccount.url) &&
89 isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
90 isAccountEndpointsObjectValid(remoteAccount.endpoint)
91}
92
93function isAccountFollowingCountValid (value: string) {
94 return exists(value) && validator.isInt('' + value, { min: 0 })
95}
96
97function isAccountFollowersCountValid (value: string) {
98 return exists(value) && validator.isInt('' + value, { min: 0 })
99}
100
101// ---------------------------------------------------------------------------
102
103export {
104 isAccountEndpointsObjectValid,
105 isAccountSharedInboxValid,
106 isAccountPublicKeyObjectValid,
107 isAccountPublicKeyIdValid,
108 isAccountTypeValid,
109 isAccountPublicKeyOwnerValid,
110 isAccountPublicKeyValid,
111 isAccountIdValid,
112 isAccountFollowingValid,
113 isAccountFollowersValid,
114 isAccountInboxValid,
115 isAccountOutboxValid,
116 isAccountPreferredUsernameValid,
117 isAccountUrlValid,
118 isAccountPrivateKeyValid,
119 isRemoteAccountValid,
120 isAccountFollowingCountValid,
121 isAccountFollowersCountValid,
122 isAccountNameValid
123}
diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts
new file mode 100644
index 000000000..800f0ddf3
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/index.ts
@@ -0,0 +1,4 @@
1export * from './account'
2export * from './signature'
3export * from './misc'
4export * from './videos'
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
new file mode 100644
index 000000000..806d33483
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -0,0 +1,17 @@
1import { exists } from '../misc'
2
3function isActivityPubUrlValid (url: string) {
4 const isURLOptions = {
5 require_host: true,
6 require_tld: true,
7 require_protocol: true,
8 require_valid_protocol: true,
9 protocols: [ 'http', 'https' ]
10 }
11
12 return exists(url) && validator.isURL(url, isURLOptions)
13}
14
15export {
16 isActivityPubUrlValid
17}
diff --git a/server/helpers/custom-validators/activitypub/signature.ts b/server/helpers/custom-validators/activitypub/signature.ts
new file mode 100644
index 000000000..683ed2b1c
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/signature.ts
@@ -0,0 +1,22 @@
1import { exists } from '../misc'
2import { isActivityPubUrlValid } from './misc'
3
4function isSignatureTypeValid (signatureType: string) {
5 return exists(signatureType) && signatureType === 'GraphSignature2012'
6}
7
8function isSignatureCreatorValid (signatureCreator: string) {
9 return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator)
10}
11
12function isSignatureValueValid (signatureValue: string) {
13 return exists(signatureValue) && signatureValue.length > 0
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 isSignatureTypeValid,
20 isSignatureCreatorValid,
21 isSignatureValueValid
22}
diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index e0ffba679..e0ffba679 100644
--- a/server/helpers/custom-validators/remote/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index c79982660..869b08870 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -1,4 +1,4 @@
1export * from './remote' 1export * from './activitypub'
2export * from './misc' 2export * from './misc'
3export * from './pods' 3export * from './pods'
4export * from './pods' 4export * from './pods'
diff --git a/server/helpers/custom-validators/remote/index.ts b/server/helpers/custom-validators/remote/index.ts
deleted file mode 100644
index e29a9b767..000000000
--- a/server/helpers/custom-validators/remote/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './videos'
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index f18b6bd9a..c07dddefe 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,4 +1,3 @@
1import * as Promise from 'bluebird'
2import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
3 2
4import { CONFIG } from '../initializers' 3import { CONFIG } from '../initializers'
diff --git a/server/helpers/index.ts b/server/helpers/index.ts
index 846bd796f..2c7ac3954 100644
--- a/server/helpers/index.ts
+++ b/server/helpers/index.ts
@@ -1,3 +1,4 @@
1export * from './activitypub'
1export * from './core-utils' 2export * from './core-utils'
2export * from './logger' 3export * from './logger'
3export * from './custom-validators' 4export * from './custom-validators'
@@ -6,3 +7,4 @@ export * from './database-utils'
6export * from './peertube-crypto' 7export * from './peertube-crypto'
7export * from './requests' 8export * from './requests'
8export * from './utils' 9export * from './utils'
10export * from './webfinger'
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 10a226af4..6d50e446f 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,77 +1,68 @@
1import * as crypto from 'crypto' 1import * as jsig from 'jsonld-signatures'
2import { join } from 'path'
3 2
4import { 3import {
5 SIGNATURE_ALGORITHM, 4 PRIVATE_RSA_KEY_SIZE,
6 SIGNATURE_ENCODING, 5 BCRYPT_SALT_SIZE
7 PRIVATE_CERT_NAME,
8 CONFIG,
9 BCRYPT_SALT_SIZE,
10 PUBLIC_CERT_NAME
11} from '../initializers' 6} from '../initializers'
12import { 7import {
13 readFilePromise,
14 bcryptComparePromise, 8 bcryptComparePromise,
15 bcryptGenSaltPromise, 9 bcryptGenSaltPromise,
16 bcryptHashPromise, 10 bcryptHashPromise,
17 accessPromise, 11 createPrivateKey,
18 opensslExecPromise 12 getPublicKey,
13 jsonldSignPromise,
14 jsonldVerifyPromise
19} from './core-utils' 15} from './core-utils'
20import { logger } from './logger' 16import { logger } from './logger'
17import { AccountInstance } from '../models/account/account-interface'
21 18
22function checkSignature (publicKey: string, data: string, hexSignature: string) { 19async function createPrivateAndPublicKeys () {
23 const verify = crypto.createVerify(SIGNATURE_ALGORITHM) 20 logger.info('Generating a RSA key...')
24
25 let dataString
26 if (typeof data === 'string') {
27 dataString = data
28 } else {
29 try {
30 dataString = JSON.stringify(data)
31 } catch (err) {
32 logger.error('Cannot check signature.', err)
33 return false
34 }
35 }
36 21
37 verify.update(dataString, 'utf8') 22 const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE)
23 const { publicKey } = await getPublicKey(key)
38 24
39 const isValid = verify.verify(publicKey, hexSignature, SIGNATURE_ENCODING) 25 return { privateKey: key, publicKey }
40 return isValid
41} 26}
42 27
43async function sign (data: string | Object) { 28function isSignatureVerified (fromAccount: AccountInstance, signedDocument: object) {
44 const sign = crypto.createSign(SIGNATURE_ALGORITHM) 29 const publicKeyObject = {
45 30 '@context': jsig.SECURITY_CONTEXT_URL,
46 let dataString: string 31 '@id': fromAccount.url,
47 if (typeof data === 'string') { 32 '@type': 'CryptographicKey',
48 dataString = data 33 owner: fromAccount.url,
49 } else { 34 publicKeyPem: fromAccount.publicKey
50 try {
51 dataString = JSON.stringify(data)
52 } catch (err) {
53 logger.error('Cannot sign data.', err)
54 return ''
55 }
56 } 35 }
57 36
58 sign.update(dataString, 'utf8') 37 const publicKeyOwnerObject = {
38 '@context': jsig.SECURITY_CONTEXT_URL,
39 '@id': fromAccount.url,
40 publicKey: [ publicKeyObject ]
41 }
59 42
60 const myKey = await getMyPrivateCert() 43 const options = {
61 return sign.sign(myKey, SIGNATURE_ENCODING) 44 publicKey: publicKeyObject,
62} 45 publicKeyOwner: publicKeyOwnerObject
46 }
63 47
64function comparePassword (plainPassword: string, hashPassword: string) { 48 return jsonldVerifyPromise(signedDocument, options)
65 return bcryptComparePromise(plainPassword, hashPassword) 49 .catch(err => {
50 logger.error('Cannot check signature.', err)
51 return false
52 })
66} 53}
67 54
68async function createCertsIfNotExist () { 55function signObject (byAccount: AccountInstance, data: any) {
69 const exist = await certsExist() 56 const options = {
70 if (exist === true) { 57 privateKeyPem: byAccount.privateKey,
71 return 58 creator: byAccount.url
72 } 59 }
73 60
74 return createCerts() 61 return jsonldSignPromise(data, options)
62}
63
64function comparePassword (plainPassword: string, hashPassword: string) {
65 return bcryptComparePromise(plainPassword, hashPassword)
75} 66}
76 67
77async function cryptPassword (password: string) { 68async function cryptPassword (password: string) {
@@ -80,69 +71,12 @@ async function cryptPassword (password: string) {
80 return bcryptHashPromise(password, salt) 71 return bcryptHashPromise(password, salt)
81} 72}
82 73
83function getMyPrivateCert () {
84 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
85 return readFilePromise(certPath, 'utf8')
86}
87
88function getMyPublicCert () {
89 const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME)
90 return readFilePromise(certPath, 'utf8')
91}
92
93// --------------------------------------------------------------------------- 74// ---------------------------------------------------------------------------
94 75
95export { 76export {
96 checkSignature, 77 isSignatureVerified,
97 comparePassword, 78 comparePassword,
98 createCertsIfNotExist, 79 createPrivateAndPublicKeys,
99 cryptPassword, 80 cryptPassword,
100 getMyPrivateCert, 81 signObject
101 getMyPublicCert,
102 sign
103}
104
105// ---------------------------------------------------------------------------
106
107async function certsExist () {
108 const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
109
110 // If there is an error the certificates do not exist
111 try {
112 await accessPromise(certPath)
113
114 return true
115 } catch {
116 return false
117 }
118}
119
120async function createCerts () {
121 const exist = await certsExist()
122 if (exist === true) {
123 const errorMessage = 'Certs already exist.'
124 logger.warning(errorMessage)
125 throw new Error(errorMessage)
126 }
127
128 logger.info('Generating a RSA key...')
129
130 const privateCertPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME)
131 const genRsaOptions = {
132 'out': privateCertPath,
133 '2048': false
134 }
135
136 await opensslExecPromise('genrsa', genRsaOptions)
137 logger.info('RSA key generated.')
138 logger.info('Managing public key...')
139
140 const publicCertPath = join(CONFIG.STORAGE.CERT_DIR, 'peertube.pub')
141 const rsaOptions = {
142 'in': privateCertPath,
143 'pubout': true,
144 'out': publicCertPath
145 }
146
147 await opensslExecPromise('rsa', rsaOptions)
148} 82}
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index af1f401de..8c4c983f7 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -9,7 +9,13 @@ import {
9} from '../initializers' 9} from '../initializers'
10import { PodInstance } from '../models' 10import { PodInstance } from '../models'
11import { PodSignature } from '../../shared' 11import { PodSignature } from '../../shared'
12import { sign } from './peertube-crypto' 12import { signObject } from './peertube-crypto'
13
14function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
15 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
16 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
17 })
18}
13 19
14type MakeRetryRequestParams = { 20type MakeRetryRequestParams = {
15 url: string, 21 url: string,
@@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) {
31} 37}
32 38
33type MakeSecureRequestParams = { 39type MakeSecureRequestParams = {
34 method: 'GET' | 'POST'
35 toPod: PodInstance 40 toPod: PodInstance
36 path: string 41 path: string
37 data?: Object 42 data?: Object
38} 43}
39function makeSecureRequest (params: MakeSecureRequestParams) { 44function makeSecureRequest (params: MakeSecureRequestParams) {
40 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { 45 const requestParams: {
41 const requestParams: { 46 method: 'POST',
42 url: string, 47 uri: string,
43 json: { 48 json: {
44 signature: PodSignature, 49 signature: PodSignature,
45 data: any 50 data: any
46 }
47 } = {
48 url: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
49 json: {
50 signature: null,
51 data: null
52 }
53 } 51 }
54 52 } = {
55 if (params.method !== 'POST') { 53 method: 'POST',
56 return rej(new Error('Cannot make a secure request with a non POST method.')) 54 uri: REMOTE_SCHEME.HTTP + '://' + params.toPod.host + params.path,
55 json: {
56 signature: null,
57 data: null
57 } 58 }
59 }
58 60
59 const host = CONFIG.WEBSERVER.HOST 61 const host = CONFIG.WEBSERVER.HOST
60 62
61 let dataToSign 63 let dataToSign
62 if (params.data) { 64 if (params.data) {
63 dataToSign = params.data 65 dataToSign = params.data
64 } else { 66 } else {
65 // We do not have data to sign so we just take our host 67 // We do not have data to sign so we just take our host
66 // It is not ideal but the connection should be in HTTPS 68 // It is not ideal but the connection should be in HTTPS
67 dataToSign = host 69 dataToSign = host
68 } 70 }
69 71
70 sign(dataToSign).then(signature => { 72 sign(dataToSign).then(signature => {
71 requestParams.json.signature = { 73 requestParams.json.signature = {
72 host, // Which host we pretend to be 74 host, // Which host we pretend to be
73 signature 75 signature
74 } 76 }
75 77
76 // If there are data information 78 // If there are data information
77 if (params.data) { 79 if (params.data) {
78 requestParams.json.data = params.data 80 requestParams.json.data = params.data
79 } 81 }
80 82
81 request.post(requestParams, (err, response, body) => err ? rej(err) : res({ response, body })) 83 return doRequest(requestParams)
82 })
83 }) 84 })
84} 85}
85 86
86// --------------------------------------------------------------------------- 87// ---------------------------------------------------------------------------
87 88
88export { 89export {
90 doRequest,
89 makeRetryRequest, 91 makeRetryRequest,
90 makeSecureRequest 92 makeSecureRequest
91} 93}
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
new file mode 100644
index 000000000..9586fa562
--- /dev/null
+++ b/server/helpers/webfinger.ts
@@ -0,0 +1,44 @@
1import * as WebFinger from 'webfinger.js'
2
3import { isTestInstance } from './core-utils'
4import { isActivityPubUrlValid } from './custom-validators'
5import { WebFingerData } from '../../shared'
6import { fetchRemoteAccountAndCreatePod } from './activitypub'
7
8const webfinger = new WebFinger({
9 webfist_fallback: false,
10 tls_only: isTestInstance(),
11 uri_fallback: false,
12 request_timeout: 3000
13})
14
15async function getAccountFromWebfinger (url: string) {
16 const webfingerData: WebFingerData = await webfingerLookup(url)
17
18 if (Array.isArray(webfingerData.links) === false) return undefined
19
20 const selfLink = webfingerData.links.find(l => l.rel === 'self')
21 if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined
22
23 const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href)
24
25 return account
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getAccountFromWebfinger
32}
33
34// ---------------------------------------------------------------------------
35
36function webfingerLookup (url: string) {
37 return new Promise<WebFingerData>((res, rej) => {
38 webfinger.lookup('nick@silverbucket.net', (err, p) => {
39 if (err) return rej(err)
40
41 return p
42 })
43 })
44}
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index 9eaef1695..b69188f7e 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -2,7 +2,7 @@ import * as config from 'config'
2 2
3import { promisify0 } from '../helpers/core-utils' 3import { promisify0 } from '../helpers/core-utils'
4import { OAuthClientModel } from '../models/oauth/oauth-client-interface' 4import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
5import { UserModel } from '../models/user/user-interface' 5import { UserModel } from '../models/account/user-interface'
6 6
7// Some checks on configuration files 7// Some checks on configuration files
8function checkConfig () { 8function checkConfig () {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index d349abaf0..cb838cf16 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -10,7 +10,8 @@ import {
10 RequestVideoEventType, 10 RequestVideoEventType,
11 RequestVideoQaduType, 11 RequestVideoQaduType,
12 RemoteVideoRequestType, 12 RemoteVideoRequestType,
13 JobState 13 JobState,
14 JobCategory
14} from '../../shared/models' 15} from '../../shared/models'
15import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum' 16import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
16 17
@@ -60,7 +61,6 @@ const CONFIG = {
60 PASSWORD: config.get<string>('database.password') 61 PASSWORD: config.get<string>('database.password')
61 }, 62 },
62 STORAGE: { 63 STORAGE: {
63 CERT_DIR: join(root(), config.get<string>('storage.certs')),
64 LOG_DIR: join(root(), config.get<string>('storage.logs')), 64 LOG_DIR: join(root(), config.get<string>('storage.logs')),
65 VIDEOS_DIR: join(root(), config.get<string>('storage.videos')), 65 VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
66 THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')), 66 THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')),
@@ -211,6 +211,10 @@ const FRIEND_SCORE = {
211 MAX: 1000 211 MAX: 1000
212} 212}
213 213
214const ACTIVITY_PUB = {
215 COLLECTION_ITEMS_PER_PAGE: 10
216}
217
214// --------------------------------------------------------------------------- 218// ---------------------------------------------------------------------------
215 219
216// Number of points we add/remove from a friend after a successful/bad request 220// Number of points we add/remove from a friend after a successful/bad request
@@ -288,17 +292,23 @@ const JOB_STATES: { [ id: string ]: JobState } = {
288 ERROR: 'error', 292 ERROR: 'error',
289 SUCCESS: 'success' 293 SUCCESS: 'success'
290} 294}
295const JOB_CATEGORIES: { [ id: string ]: JobCategory } = {
296 TRANSCODING: 'transcoding',
297 HTTP_REQUEST: 'http-request'
298}
291// How many maximum jobs we fetch from the database per cycle 299// How many maximum jobs we fetch from the database per cycle
292const JOBS_FETCH_LIMIT_PER_CYCLE = 10 300const JOBS_FETCH_LIMIT_PER_CYCLE = {
301 transcoding: 10,
302 httpRequest: 20
303}
293// 1 minutes 304// 1 minutes
294let JOBS_FETCHING_INTERVAL = 60000 305let JOBS_FETCHING_INTERVAL = 60000
295 306
296// --------------------------------------------------------------------------- 307// ---------------------------------------------------------------------------
297 308
298const PRIVATE_CERT_NAME = 'peertube.key.pem' 309// const SIGNATURE_ALGORITHM = 'RSA-SHA256'
299const PUBLIC_CERT_NAME = 'peertube.pub' 310// const SIGNATURE_ENCODING = 'hex'
300const SIGNATURE_ALGORITHM = 'RSA-SHA256' 311const PRIVATE_RSA_KEY_SIZE = 2048
301const SIGNATURE_ENCODING = 'hex'
302 312
303// Password encryption 313// Password encryption
304const BCRYPT_SALT_SIZE = 10 314const BCRYPT_SALT_SIZE = 10
@@ -368,14 +378,13 @@ export {
368 JOB_STATES, 378 JOB_STATES,
369 JOBS_FETCH_LIMIT_PER_CYCLE, 379 JOBS_FETCH_LIMIT_PER_CYCLE,
370 JOBS_FETCHING_INTERVAL, 380 JOBS_FETCHING_INTERVAL,
381 JOB_CATEGORIES,
371 LAST_MIGRATION_VERSION, 382 LAST_MIGRATION_VERSION,
372 OAUTH_LIFETIME, 383 OAUTH_LIFETIME,
373 OPENGRAPH_AND_OEMBED_COMMENT, 384 OPENGRAPH_AND_OEMBED_COMMENT,
374 PAGINATION_COUNT_DEFAULT, 385 PAGINATION_COUNT_DEFAULT,
375 PODS_SCORE, 386 PODS_SCORE,
376 PREVIEWS_SIZE, 387 PREVIEWS_SIZE,
377 PRIVATE_CERT_NAME,
378 PUBLIC_CERT_NAME,
379 REMOTE_SCHEME, 388 REMOTE_SCHEME,
380 REQUEST_ENDPOINT_ACTIONS, 389 REQUEST_ENDPOINT_ACTIONS,
381 REQUEST_ENDPOINTS, 390 REQUEST_ENDPOINTS,
@@ -393,11 +402,11 @@ export {
393 REQUESTS_VIDEO_QADU_LIMIT_PODS, 402 REQUESTS_VIDEO_QADU_LIMIT_PODS,
394 RETRY_REQUESTS, 403 RETRY_REQUESTS,
395 SEARCHABLE_COLUMNS, 404 SEARCHABLE_COLUMNS,
396 SIGNATURE_ALGORITHM, 405 PRIVATE_RSA_KEY_SIZE,
397 SIGNATURE_ENCODING,
398 SORTABLE_COLUMNS, 406 SORTABLE_COLUMNS,
399 STATIC_MAX_AGE, 407 STATIC_MAX_AGE,
400 STATIC_PATHS, 408 STATIC_PATHS,
409 ACTIVITY_PUB,
401 THUMBNAILS_SIZE, 410 THUMBNAILS_SIZE,
402 VIDEO_CATEGORIES, 411 VIDEO_CATEGORIES,
403 VIDEO_LANGUAGES, 412 VIDEO_LANGUAGES,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 141566c3a..52e766394 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -15,8 +15,9 @@ import { BlacklistedVideoModel } from './../models/video/video-blacklist-interfa
15import { VideoFileModel } from './../models/video/video-file-interface' 15import { VideoFileModel } from './../models/video/video-file-interface'
16import { VideoAbuseModel } from './../models/video/video-abuse-interface' 16import { VideoAbuseModel } from './../models/video/video-abuse-interface'
17import { VideoChannelModel } from './../models/video/video-channel-interface' 17import { VideoChannelModel } from './../models/video/video-channel-interface'
18import { UserModel } from './../models/user/user-interface' 18import { UserModel } from '../models/account/user-interface'
19import { UserVideoRateModel } from './../models/user/user-video-rate-interface' 19import { AccountVideoRateModel } from '../models/account/account-video-rate-interface'
20import { AccountFollowModel } from '../models/account/account-follow-interface'
20import { TagModel } from './../models/video/tag-interface' 21import { TagModel } from './../models/video/tag-interface'
21import { RequestModel } from './../models/request/request-interface' 22import { RequestModel } from './../models/request/request-interface'
22import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface' 23import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface'
@@ -26,7 +27,7 @@ import { PodModel } from './../models/pod/pod-interface'
26import { OAuthTokenModel } from './../models/oauth/oauth-token-interface' 27import { OAuthTokenModel } from './../models/oauth/oauth-token-interface'
27import { OAuthClientModel } from './../models/oauth/oauth-client-interface' 28import { OAuthClientModel } from './../models/oauth/oauth-client-interface'
28import { JobModel } from './../models/job/job-interface' 29import { JobModel } from './../models/job/job-interface'
29import { AuthorModel } from './../models/video/author-interface' 30import { AccountModel } from './../models/account/account-interface'
30import { ApplicationModel } from './../models/application/application-interface' 31import { ApplicationModel } from './../models/application/application-interface'
31 32
32const dbname = CONFIG.DATABASE.DBNAME 33const dbname = CONFIG.DATABASE.DBNAME
@@ -38,7 +39,7 @@ const database: {
38 init?: (silent: boolean) => Promise<void>, 39 init?: (silent: boolean) => Promise<void>,
39 40
40 Application?: ApplicationModel, 41 Application?: ApplicationModel,
41 Author?: AuthorModel, 42 Account?: AccountModel,
42 Job?: JobModel, 43 Job?: JobModel,
43 OAuthClient?: OAuthClientModel, 44 OAuthClient?: OAuthClientModel,
44 OAuthToken?: OAuthTokenModel, 45 OAuthToken?: OAuthTokenModel,
@@ -48,7 +49,8 @@ const database: {
48 RequestVideoQadu?: RequestVideoQaduModel, 49 RequestVideoQadu?: RequestVideoQaduModel,
49 Request?: RequestModel, 50 Request?: RequestModel,
50 Tag?: TagModel, 51 Tag?: TagModel,
51 UserVideoRate?: UserVideoRateModel, 52 AccountVideoRate?: AccountVideoRateModel,
53 AccountFollow?: AccountFollowModel,
52 User?: UserModel, 54 User?: UserModel,
53 VideoAbuse?: VideoAbuseModel, 55 VideoAbuse?: VideoAbuseModel,
54 VideoChannel?: VideoChannelModel, 56 VideoChannel?: VideoChannelModel,
@@ -126,7 +128,7 @@ async function getModelFiles (modelDirectory: string) {
126 return true 128 return true
127 }) 129 })
128 130
129 const tasks: Bluebird<any>[] = [] 131 const tasks: Promise<any>[] = []
130 132
131 // For each directory we read it and append model in the modelFilePaths array 133 // For each directory we read it and append model in the modelFilePaths array
132 for (const directory of directories) { 134 for (const directory of directories) {
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts
new file mode 100644
index 000000000..740800606
--- /dev/null
+++ b/server/lib/activitypub/index.ts
@@ -0,0 +1,3 @@
1export * from './process-create'
2export * from './process-flag'
3export * from './process-update'
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
new file mode 100644
index 000000000..114ff1848
--- /dev/null
+++ b/server/lib/activitypub/process-create.ts
@@ -0,0 +1,104 @@
1import {
2 ActivityCreate,
3 VideoTorrentObject,
4 VideoChannelObject
5} from '../../../shared'
6import { database as db } from '../../initializers'
7import { logger, retryTransactionWrapper } from '../../helpers'
8
9function processCreateActivity (activity: ActivityCreate) {
10 const activityObject = activity.object
11 const activityType = activityObject.type
12
13 if (activityType === 'Video') {
14 return processCreateVideo(activityObject as VideoTorrentObject)
15 } else if (activityType === 'VideoChannel') {
16 return processCreateVideoChannel(activityObject as VideoChannelObject)
17 }
18
19 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
20 return Promise.resolve()
21}
22
23// ---------------------------------------------------------------------------
24
25export {
26 processCreateActivity
27}
28
29// ---------------------------------------------------------------------------
30
31function processCreateVideo (video: VideoTorrentObject) {
32 const options = {
33 arguments: [ video ],
34 errorMessage: 'Cannot insert the remote video with many retries.'
35 }
36
37 return retryTransactionWrapper(addRemoteVideo, options)
38}
39
40async function addRemoteVideo (videoToCreateData: VideoTorrentObject) {
41 logger.debug('Adding remote video %s.', videoToCreateData.url)
42
43 await db.sequelize.transaction(async t => {
44 const sequelizeOptions = {
45 transaction: t
46 }
47
48 const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid)
49 if (videoFromDatabase) throw new Error('UUID already exists.')
50
51 const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t)
52 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.')
53
54 const tags = videoToCreateData.tags
55 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
56
57 const videoData = {
58 name: videoToCreateData.name,
59 uuid: videoToCreateData.uuid,
60 category: videoToCreateData.category,
61 licence: videoToCreateData.licence,
62 language: videoToCreateData.language,
63 nsfw: videoToCreateData.nsfw,
64 description: videoToCreateData.truncatedDescription,
65 channelId: videoChannel.id,
66 duration: videoToCreateData.duration,
67 createdAt: videoToCreateData.createdAt,
68 // FIXME: updatedAt does not seems to be considered by Sequelize
69 updatedAt: videoToCreateData.updatedAt,
70 views: videoToCreateData.views,
71 likes: videoToCreateData.likes,
72 dislikes: videoToCreateData.dislikes,
73 remote: true,
74 privacy: videoToCreateData.privacy
75 }
76
77 const video = db.Video.build(videoData)
78 await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
79 const videoCreated = await video.save(sequelizeOptions)
80
81 const tasks = []
82 for (const fileData of videoToCreateData.files) {
83 const videoFileInstance = db.VideoFile.build({
84 extname: fileData.extname,
85 infoHash: fileData.infoHash,
86 resolution: fileData.resolution,
87 size: fileData.size,
88 videoId: videoCreated.id
89 })
90
91 tasks.push(videoFileInstance.save(sequelizeOptions))
92 }
93
94 await Promise.all(tasks)
95
96 await videoCreated.setTags(tagInstances, sequelizeOptions)
97 })
98
99 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
100}
101
102function processCreateVideoChannel (videoChannel: VideoChannelObject) {
103
104}
diff --git a/server/lib/activitypub/process-flag.ts b/server/lib/activitypub/process-flag.ts
new file mode 100644
index 000000000..6fa862ee9
--- /dev/null
+++ b/server/lib/activitypub/process-flag.ts
@@ -0,0 +1,17 @@
1import {
2 ActivityCreate,
3 VideoTorrentObject,
4 VideoChannelObject
5} from '../../../shared'
6
7function processFlagActivity (activity: ActivityCreate) {
8 // empty
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 processFlagActivity
15}
16
17// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts
new file mode 100644
index 000000000..187c7be7c
--- /dev/null
+++ b/server/lib/activitypub/process-update.ts
@@ -0,0 +1,29 @@
1import {
2 ActivityCreate,
3 VideoTorrentObject,
4 VideoChannelObject
5} from '../../../shared'
6
7function processUpdateActivity (activity: ActivityCreate) {
8 if (activity.object.type === 'Video') {
9 return processUpdateVideo(activity.object)
10 } else if (activity.object.type === 'VideoChannel') {
11 return processUpdateVideoChannel(activity.object)
12 }
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 processUpdateActivity
19}
20
21// ---------------------------------------------------------------------------
22
23function processUpdateVideo (video: VideoTorrentObject) {
24
25}
26
27function processUpdateVideoChannel (videoChannel: VideoChannelObject) {
28
29}
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
new file mode 100644
index 000000000..6a31c226d
--- /dev/null
+++ b/server/lib/activitypub/send-request.ts
@@ -0,0 +1,129 @@
1import * as Sequelize from 'sequelize'
2
3import {
4 AccountInstance,
5 VideoInstance,
6 VideoChannelInstance
7} from '../../models'
8import { httpRequestJobScheduler } from '../jobs'
9import { signObject, activityPubContextify } from '../../helpers'
10import { Activity } from '../../../shared'
11
12function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
13 const videoChannelObject = videoChannel.toActivityPubObject()
14 const data = createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
15
16 return broadcastToFollowers(data, t)
17}
18
19function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
20 const videoChannelObject = videoChannel.toActivityPubObject()
21 const data = updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
22
23 return broadcastToFollowers(data, t)
24}
25
26function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
27 const videoChannelObject = videoChannel.toActivityPubObject()
28 const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
29
30 return broadcastToFollowers(data, t)
31}
32
33function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
34 const videoObject = video.toActivityPubObject()
35 const data = addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
36
37 return broadcastToFollowers(data, t)
38}
39
40function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
41 const videoObject = video.toActivityPubObject()
42 const data = updateActivityData(video.url, video.VideoChannel.Account, videoObject)
43
44 return broadcastToFollowers(data, t)
45}
46
47function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
48 const videoObject = video.toActivityPubObject()
49 const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject)
50
51 return broadcastToFollowers(data, t)
52}
53
54// ---------------------------------------------------------------------------
55
56export {
57
58}
59
60// ---------------------------------------------------------------------------
61
62function broadcastToFollowers (data: any, t: Sequelize.Transaction) {
63 return httpRequestJobScheduler.createJob(t, 'http-request', 'httpRequestBroadcastHandler', data)
64}
65
66function buildSignedActivity (byAccount: AccountInstance, data: Object) {
67 const activity = activityPubContextify(data)
68
69 return signObject(byAccount, activity) as Promise<Activity>
70}
71
72async function getPublicActivityTo (account: AccountInstance) {
73 const inboxUrls = await account.getFollowerSharedInboxUrls()
74
75 return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public')
76}
77
78async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
79 const to = await getPublicActivityTo(byAccount)
80 const base = {
81 type: 'Create',
82 id: url,
83 actor: byAccount.url,
84 to,
85 object
86 }
87
88 return buildSignedActivity(byAccount, base)
89}
90
91async function updateActivityData (url: string, byAccount: AccountInstance, object: any) {
92 const to = await getPublicActivityTo(byAccount)
93 const base = {
94 type: 'Update',
95 id: url,
96 actor: byAccount.url,
97 to,
98 object
99 }
100
101 return buildSignedActivity(byAccount, base)
102}
103
104async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) {
105 const to = await getPublicActivityTo(byAccount)
106 const base = {
107 type: 'Update',
108 id: url,
109 actor: byAccount.url,
110 to,
111 object
112 }
113
114 return buildSignedActivity(byAccount, base)
115}
116
117async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) {
118 const to = await getPublicActivityTo(byAccount)
119 const base = {
120 type: 'Add',
121 id: url,
122 actor: byAccount.url,
123 to,
124 object,
125 target
126 }
127
128 return buildSignedActivity(byAccount, base)
129}
diff --git a/server/lib/index.ts b/server/lib/index.ts
index d1534b085..bfb415ad2 100644
--- a/server/lib/index.ts
+++ b/server/lib/index.ts
@@ -1,3 +1,4 @@
1export * from './activitypub'
1export * from './cache' 2export * from './cache'
2export * from './jobs' 3export * from './jobs'
3export * from './request' 4export * from './request'
diff --git a/server/lib/jobs/handlers/index.ts b/server/lib/jobs/handlers/index.ts
deleted file mode 100644
index cef1f89a9..000000000
--- a/server/lib/jobs/handlers/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
1import * as videoFileOptimizer from './video-file-optimizer'
2import * as videoFileTranscoder from './video-file-transcoder'
3
4export interface JobHandler<T> {
5 process (data: object, jobId: number): T
6 onError (err: Error, jobId: number)
7 onSuccess (jobId: number, jobResult: T)
8}
9
10const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
11 videoFileOptimizer,
12 videoFileTranscoder
13}
14
15export {
16 jobHandlers
17}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
new file mode 100644
index 000000000..6b6946d02
--- /dev/null
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
@@ -0,0 +1,25 @@
1import * as Bluebird from 'bluebird'
2
3import { database as db } from '../../../initializers/database'
4import { logger } from '../../../helpers'
5
6async function process (data: { videoUUID: string }, jobId: number) {
7
8}
9
10function onError (err: Error, jobId: number) {
11 logger.error('Error when optimized video file in job %d.', jobId, err)
12 return Promise.resolve()
13}
14
15async function onSuccess (jobId: number) {
16
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 process,
23 onError,
24 onSuccess
25}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts
new file mode 100644
index 000000000..42cb9139c
--- /dev/null
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-job-scheduler.ts
@@ -0,0 +1,17 @@
1import { JobScheduler, JobHandler } from '../job-scheduler'
2
3import * as httpRequestBroadcastHandler from './http-request-broadcast-handler'
4import * as httpRequestUnicastHandler from './http-request-unicast-handler'
5import { JobCategory } from '../../../../shared'
6
7const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
8 httpRequestBroadcastHandler,
9 httpRequestUnicastHandler
10}
11const jobCategory: JobCategory = 'http-request'
12
13const httpRequestJobScheduler = new JobScheduler(jobCategory, jobHandlers)
14
15export {
16 httpRequestJobScheduler
17}
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
new file mode 100644
index 000000000..6b6946d02
--- /dev/null
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
@@ -0,0 +1,25 @@
1import * as Bluebird from 'bluebird'
2
3import { database as db } from '../../../initializers/database'
4import { logger } from '../../../helpers'
5
6async function process (data: { videoUUID: string }, jobId: number) {
7
8}
9
10function onError (err: Error, jobId: number) {
11 logger.error('Error when optimized video file in job %d.', jobId, err)
12 return Promise.resolve()
13}
14
15async function onSuccess (jobId: number) {
16
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 process,
23 onError,
24 onSuccess
25}
diff --git a/server/lib/jobs/http-request-job-scheduler/index.ts b/server/lib/jobs/http-request-job-scheduler/index.ts
new file mode 100644
index 000000000..4d2573296
--- /dev/null
+++ b/server/lib/jobs/http-request-job-scheduler/index.ts
@@ -0,0 +1 @@
export * from './http-request-job-scheduler'
diff --git a/server/lib/jobs/index.ts b/server/lib/jobs/index.ts
index b18a3d845..a92743707 100644
--- a/server/lib/jobs/index.ts
+++ b/server/lib/jobs/index.ts
@@ -1 +1,2 @@
1export * from './job-scheduler' 1export * from './http-request-job-scheduler'
2export * from './transcoding-job-scheduler'
diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts
index 61d483268..89a4bca88 100644
--- a/server/lib/jobs/job-scheduler.ts
+++ b/server/lib/jobs/job-scheduler.ts
@@ -1,39 +1,41 @@
1import { AsyncQueue, forever, queue } from 'async' 1import { AsyncQueue, forever, queue } from 'async'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3 3
4import { database as db } from '../../initializers/database'
5import { 4import {
5 database as db,
6 JOBS_FETCHING_INTERVAL, 6 JOBS_FETCHING_INTERVAL,
7 JOBS_FETCH_LIMIT_PER_CYCLE, 7 JOBS_FETCH_LIMIT_PER_CYCLE,
8 JOB_STATES 8 JOB_STATES
9} from '../../initializers' 9} from '../../initializers'
10import { logger } from '../../helpers' 10import { logger } from '../../helpers'
11import { JobInstance } from '../../models' 11import { JobInstance } from '../../models'
12import { JobHandler, jobHandlers } from './handlers' 12import { JobCategory } from '../../../shared'
13 13
14export interface JobHandler<T> {
15 process (data: object, jobId: number): T
16 onError (err: Error, jobId: number)
17 onSuccess (jobId: number, jobResult: T)
18}
14type JobQueueCallback = (err: Error) => void 19type JobQueueCallback = (err: Error) => void
15 20
16class JobScheduler { 21class JobScheduler<T> {
17
18 private static instance: JobScheduler
19 22
20 private constructor () { } 23 constructor (
21 24 private jobCategory: JobCategory,
22 static get Instance () { 25 private jobHandlers: { [ id: string ]: JobHandler<T> }
23 return this.instance || (this.instance = new this()) 26 ) {}
24 }
25 27
26 async activate () { 28 async activate () {
27 const limit = JOBS_FETCH_LIMIT_PER_CYCLE 29 const limit = JOBS_FETCH_LIMIT_PER_CYCLE[this.jobCategory]
28 30
29 logger.info('Jobs scheduler activated.') 31 logger.info('Jobs scheduler %s activated.', this.jobCategory)
30 32
31 const jobsQueue = queue<JobInstance, JobQueueCallback>(this.processJob.bind(this)) 33 const jobsQueue = queue<JobInstance, JobQueueCallback>(this.processJob.bind(this))
32 34
33 // Finish processing jobs from a previous start 35 // Finish processing jobs from a previous start
34 const state = JOB_STATES.PROCESSING 36 const state = JOB_STATES.PROCESSING
35 try { 37 try {
36 const jobs = await db.Job.listWithLimit(limit, state) 38 const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
37 39
38 this.enqueueJobs(jobsQueue, jobs) 40 this.enqueueJobs(jobsQueue, jobs)
39 } catch (err) { 41 } catch (err) {
@@ -49,7 +51,7 @@ class JobScheduler {
49 51
50 const state = JOB_STATES.PENDING 52 const state = JOB_STATES.PENDING
51 try { 53 try {
52 const jobs = await db.Job.listWithLimit(limit, state) 54 const jobs = await db.Job.listWithLimitByCategory(limit, state, this.jobCategory)
53 55
54 this.enqueueJobs(jobsQueue, jobs) 56 this.enqueueJobs(jobsQueue, jobs)
55 } catch (err) { 57 } catch (err) {
@@ -64,9 +66,10 @@ class JobScheduler {
64 ) 66 )
65 } 67 }
66 68
67 createJob (transaction: Sequelize.Transaction, handlerName: string, handlerInputData: object) { 69 createJob (transaction: Sequelize.Transaction, category: JobCategory, handlerName: string, handlerInputData: object) {
68 const createQuery = { 70 const createQuery = {
69 state: JOB_STATES.PENDING, 71 state: JOB_STATES.PENDING,
72 category,
70 handlerName, 73 handlerName,
71 handlerInputData 74 handlerInputData
72 } 75 }
@@ -80,7 +83,7 @@ class JobScheduler {
80 } 83 }
81 84
82 private async processJob (job: JobInstance, callback: (err: Error) => void) { 85 private async processJob (job: JobInstance, callback: (err: Error) => void) {
83 const jobHandler = jobHandlers[job.handlerName] 86 const jobHandler = this.jobHandlers[job.handlerName]
84 if (jobHandler === undefined) { 87 if (jobHandler === undefined) {
85 logger.error('Unknown job handler for job %s.', job.handlerName) 88 logger.error('Unknown job handler for job %s.', job.handlerName)
86 return callback(null) 89 return callback(null)
diff --git a/server/lib/jobs/transcoding-job-scheduler/index.ts b/server/lib/jobs/transcoding-job-scheduler/index.ts
new file mode 100644
index 000000000..73152a1be
--- /dev/null
+++ b/server/lib/jobs/transcoding-job-scheduler/index.ts
@@ -0,0 +1 @@
export * from './transcoding-job-scheduler'
diff --git a/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts b/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts
new file mode 100644
index 000000000..d7c614fb8
--- /dev/null
+++ b/server/lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler.ts
@@ -0,0 +1,17 @@
1import { JobScheduler, JobHandler } from '../job-scheduler'
2
3import * as videoFileOptimizer from './video-file-optimizer-handler'
4import * as videoFileTranscoder from './video-file-transcoder-handler'
5import { JobCategory } from '../../../../shared'
6
7const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = {
8 videoFileOptimizer,
9 videoFileTranscoder
10}
11const jobCategory: JobCategory = 'transcoding'
12
13const transcodingJobScheduler = new JobScheduler(jobCategory, jobHandlers)
14
15export {
16 transcodingJobScheduler
17}
diff --git a/server/lib/jobs/handlers/video-file-optimizer.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts
index ccded4721..ccded4721 100644
--- a/server/lib/jobs/handlers/video-file-optimizer.ts
+++ b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts
diff --git a/server/lib/jobs/handlers/video-file-transcoder.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts
index 853645510..853645510 100644
--- a/server/lib/jobs/handlers/video-file-transcoder.ts
+++ b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts
diff --git a/server/lib/user.ts b/server/lib/user.ts
index a92f4777b..57c653e55 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,9 +1,9 @@
1import { database as db } from '../initializers' 1import { database as db } from '../initializers'
2import { UserInstance } from '../models' 2import { UserInstance } from '../models'
3import { addVideoAuthorToFriends } from './friends' 3import { addVideoAccountToFriends } from './friends'
4import { createVideoChannel } from './video-channel' 4import { createVideoChannel } from './video-channel'
5 5
6async function createUserAuthorAndChannel (user: UserInstance, validateUser = true) { 6async function createUserAccountAndChannel (user: UserInstance, validateUser = true) {
7 const res = await db.sequelize.transaction(async t => { 7 const res = await db.sequelize.transaction(async t => {
8 const userOptions = { 8 const userOptions = {
9 transaction: t, 9 transaction: t,
@@ -11,25 +11,25 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
11 } 11 }
12 12
13 const userCreated = await user.save(userOptions) 13 const userCreated = await user.save(userOptions)
14 const authorInstance = db.Author.build({ 14 const accountInstance = db.Account.build({
15 name: userCreated.username, 15 name: userCreated.username,
16 podId: null, // It is our pod 16 podId: null, // It is our pod
17 userId: userCreated.id 17 userId: userCreated.id
18 }) 18 })
19 19
20 const authorCreated = await authorInstance.save({ transaction: t }) 20 const accountCreated = await accountInstance.save({ transaction: t })
21 21
22 const remoteVideoAuthor = authorCreated.toAddRemoteJSON() 22 const remoteVideoAccount = accountCreated.toAddRemoteJSON()
23 23
24 // Now we'll add the video channel's meta data to our friends 24 // Now we'll add the video channel's meta data to our friends
25 const author = await addVideoAuthorToFriends(remoteVideoAuthor, t) 25 const account = await addVideoAccountToFriends(remoteVideoAccount, t)
26 26
27 const videoChannelInfo = { 27 const videoChannelInfo = {
28 name: `Default ${userCreated.username} channel` 28 name: `Default ${userCreated.username} channel`
29 } 29 }
30 const videoChannel = await createVideoChannel(videoChannelInfo, authorCreated, t) 30 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
31 31
32 return { author, videoChannel } 32 return { account, videoChannel }
33 }) 33 })
34 34
35 return res 35 return res
@@ -38,5 +38,5 @@ async function createUserAuthorAndChannel (user: UserInstance, validateUser = tr
38// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
39 39
40export { 40export {
41 createUserAuthorAndChannel 41 createUserAccountAndChannel
42} 42}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 678ffe643..a6dd4d061 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,15 +3,15 @@ import * as Sequelize from 'sequelize'
3import { addVideoChannelToFriends } from './friends' 3import { addVideoChannelToFriends } from './friends'
4import { database as db } from '../initializers' 4import { database as db } from '../initializers'
5import { logger } from '../helpers' 5import { logger } from '../helpers'
6import { AuthorInstance } from '../models' 6import { AccountInstance } from '../models'
7import { VideoChannelCreate } from '../../shared/models' 7import { VideoChannelCreate } from '../../shared/models'
8 8
9async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) { 9async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) {
10 const videoChannelData = { 10 const videoChannelData = {
11 name: videoChannelInfo.name, 11 name: videoChannelInfo.name,
12 description: videoChannelInfo.description, 12 description: videoChannelInfo.description,
13 remote: false, 13 remote: false,
14 authorId: author.id 14 authorId: account.id
15 } 15 }
16 16
17 const videoChannel = db.VideoChannel.build(videoChannelData) 17 const videoChannel = db.VideoChannel.build(videoChannelData)
@@ -19,8 +19,8 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author:
19 19
20 const videoChannelCreated = await videoChannel.save(options) 20 const videoChannelCreated = await videoChannel.save(options)
21 21
22 // Do not forget to add Author information to the created video channel 22 // Do not forget to add Account information to the created video channel
23 videoChannelCreated.Author = author 23 videoChannelCreated.Account = account
24 24
25 const remoteVideoChannel = videoChannelCreated.toAddRemoteJSON() 25 const remoteVideoChannel = videoChannelCreated.toAddRemoteJSON()
26 26
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
new file mode 100644
index 000000000..6cf8eea6f
--- /dev/null
+++ b/server/middlewares/activitypub.ts
@@ -0,0 +1,57 @@
1import { Request, Response, NextFunction } from 'express'
2
3import { database as db } from '../initializers'
4import {
5 logger,
6 getAccountFromWebfinger,
7 isSignatureVerified
8} from '../helpers'
9import { ActivityPubSignature } from '../../shared'
10
11async function checkSignature (req: Request, res: Response, next: NextFunction) {
12 const signatureObject: ActivityPubSignature = req.body.signature
13
14 logger.debug('Checking signature of account %s...', signatureObject.creator)
15
16 let account = await db.Account.loadByUrl(signatureObject.creator)
17
18 // We don't have this account in our database, fetch it on remote
19 if (!account) {
20 account = await getAccountFromWebfinger(signatureObject.creator)
21
22 if (!account) {
23 return res.sendStatus(403)
24 }
25
26 // Save our new account in database
27 await account.save()
28 }
29
30 const verified = await isSignatureVerified(account, req.body)
31 if (verified === false) return res.sendStatus(403)
32
33 res.locals.signature.account = account
34
35 return next()
36}
37
38function executeIfActivityPub (fun: any | any[]) {
39 return (req: Request, res: Response, next: NextFunction) => {
40 if (req.header('Accept') !== 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') {
41 return next()
42 }
43
44 if (Array.isArray(fun) === true) {
45 fun[0](req, res, next) // FIXME: doesn't work
46 }
47
48 return fun(req, res, next)
49 }
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 checkSignature,
56 executeIfActivityPub
57}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index cec3e0b2a..40480450b 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -1,9 +1,9 @@
1export * from './validators' 1export * from './validators'
2export * from './activitypub'
2export * from './async' 3export * from './async'
3export * from './oauth' 4export * from './oauth'
4export * from './pagination' 5export * from './pagination'
5export * from './pods' 6export * from './pods'
6export * from './search' 7export * from './search'
7export * from './secure'
8export * from './sort' 8export * from './sort'
9export * from './user-right' 9export * from './user-right'
diff --git a/server/middlewares/secure.ts b/server/middlewares/secure.ts
deleted file mode 100644
index 5dd809f15..000000000
--- a/server/middlewares/secure.ts
+++ /dev/null
@@ -1,55 +0,0 @@
1import 'express-validator'
2import * as express from 'express'
3
4import { database as db } from '../initializers'
5import {
6 logger,
7 checkSignature as peertubeCryptoCheckSignature
8} from '../helpers'
9import { PodSignature } from '../../shared'
10
11async function checkSignature (req: express.Request, res: express.Response, next: express.NextFunction) {
12 const signatureObject: PodSignature = req.body.signature
13 const host = signatureObject.host
14
15 try {
16 const pod = await db.Pod.loadByHost(host)
17 if (pod === null) {
18 logger.error('Unknown pod %s.', host)
19 return res.sendStatus(403)
20 }
21
22 logger.debug('Checking signature from %s.', host)
23
24 let signatureShouldBe
25 // If there is data in the body the sender used it for its signature
26 // If there is no data we just use its host as signature
27 if (req.body.data) {
28 signatureShouldBe = req.body.data
29 } else {
30 signatureShouldBe = host
31 }
32
33 const signatureOk = peertubeCryptoCheckSignature(pod.publicKey, signatureShouldBe, signatureObject.signature)
34
35 if (signatureOk === true) {
36 res.locals.secure = {
37 pod
38 }
39
40 return next()
41 }
42
43 logger.error('Signature is not okay in body for %s.', signatureObject.host)
44 return res.sendStatus(403)
45 } catch (err) {
46 logger.error('Cannot get signed host in body.', { error: err.stack, signature: signatureObject.signature })
47 return res.sendStatus(500)
48 }
49}
50
51// ---------------------------------------------------------------------------
52
53export {
54 checkSignature
55}
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
new file mode 100644
index 000000000..5abe942d6
--- /dev/null
+++ b/server/middlewares/validators/account.ts
@@ -0,0 +1,53 @@
1import { param } from 'express-validator/check'
2import * as express from 'express'
3
4import { database as db } from '../../initializers/database'
5import { checkErrors } from './utils'
6import {
7 logger,
8 isUserUsernameValid,
9 isUserPasswordValid,
10 isUserVideoQuotaValid,
11 isUserDisplayNSFWValid,
12 isUserRoleValid,
13 isAccountNameValid
14} from '../../helpers'
15import { AccountInstance } from '../../models'
16
17const localAccountValidator = [
18 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
19
20 (req: express.Request, res: express.Response, next: express.NextFunction) => {
21 logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
22
23 checkErrors(req, res, () => {
24 checkLocalAccountExists(req.params.name, res, next)
25 })
26 }
27]
28
29// ---------------------------------------------------------------------------
30
31export {
32 localAccountValidator
33}
34
35// ---------------------------------------------------------------------------
36
37function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
38 db.Account.loadLocalAccountByName(name)
39 .then(account => {
40 if (!account) {
41 return res.status(404)
42 .send({ error: 'Account not found' })
43 .end()
44 }
45
46 res.locals.account = account
47 return callback(null, account)
48 })
49 .catch(err => {
50 logger.error('Error in account request validator.', err)
51 return res.sendStatus(500)
52 })
53}
diff --git a/server/middlewares/validators/remote/index.ts b/server/middlewares/validators/activitypub/index.ts
index f1f26043e..f1f26043e 100644
--- a/server/middlewares/validators/remote/index.ts
+++ b/server/middlewares/validators/activitypub/index.ts
diff --git a/server/middlewares/validators/remote/pods.ts b/server/middlewares/validators/activitypub/pods.ts
index f917b61ee..f917b61ee 100644
--- a/server/middlewares/validators/remote/pods.ts
+++ b/server/middlewares/validators/activitypub/pods.ts
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
new file mode 100644
index 000000000..0ce15c1f6
--- /dev/null
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -0,0 +1,30 @@
1import { body } from 'express-validator/check'
2import * as express from 'express'
3
4import {
5 logger,
6 isDateValid,
7 isSignatureTypeValid,
8 isSignatureCreatorValid,
9 isSignatureValueValid
10} from '../../../helpers'
11import { checkErrors } from '../utils'
12
13const signatureValidator = [
14 body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
15 body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'),
16 body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
17 body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
18
19 (req: express.Request, res: express.Response, next: express.NextFunction) => {
20 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } })
21
22 checkErrors(req, res, next)
23 }
24]
25
26// ---------------------------------------------------------------------------
27
28export {
29 signatureValidator
30}
diff --git a/server/middlewares/validators/remote/videos.ts b/server/middlewares/validators/activitypub/videos.ts
index 497320cc1..497320cc1 100644
--- a/server/middlewares/validators/remote/videos.ts
+++ b/server/middlewares/validators/activitypub/videos.ts
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 247f6039e..46c00d679 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,5 +1,6 @@
1export * from './account'
1export * from './oembed' 2export * from './oembed'
2export * from './remote' 3export * from './activitypub'
3export * from './pagination' 4export * from './pagination'
4export * from './pods' 5export * from './pods'
5export * from './sort' 6export * from './sort'
diff --git a/server/middlewares/validators/remote/signature.ts b/server/middlewares/validators/remote/signature.ts
deleted file mode 100644
index d3937b515..000000000
--- a/server/middlewares/validators/remote/signature.ts
+++ /dev/null
@@ -1,22 +0,0 @@
1import { body } from 'express-validator/check'
2import * as express from 'express'
3
4import { logger, isHostValid } from '../../../helpers'
5import { checkErrors } from '../utils'
6
7const signatureValidator = [
8 body('signature.host').custom(isHostValid).withMessage('Should have a signature host'),
9 body('signature.signature').not().isEmpty().withMessage('Should have a signature'),
10
11 (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 logger.debug('Checking signature parameters', { parameters: { signature: req.body.signature } })
13
14 checkErrors(req, res, next)
15 }
16]
17
18// ---------------------------------------------------------------------------
19
20export {
21 signatureValidator
22}
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
new file mode 100644
index 000000000..3be383649
--- /dev/null
+++ b/server/models/account/account-follow-interface.ts
@@ -0,0 +1,23 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace AccountFollowMethods {
7}
8
9export interface AccountFollowClass {
10}
11
12export interface AccountFollowAttributes {
13 accountId: number
14 targetAccountId: number
15}
16
17export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
18 id: number
19 createdAt: Date
20 updatedAt: Date
21}
22
23export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
new file mode 100644
index 000000000..9bf03b253
--- /dev/null
+++ b/server/models/account/account-follow.ts
@@ -0,0 +1,56 @@
1import * as Sequelize from 'sequelize'
2
3import { addMethodsToModel } from '../utils'
4import {
5 AccountFollowInstance,
6 AccountFollowAttributes,
7
8 AccountFollowMethods
9} from './account-follow-interface'
10
11let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
12
13export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
14 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
15 { },
16 {
17 indexes: [
18 {
19 fields: [ 'accountId' ],
20 unique: true
21 },
22 {
23 fields: [ 'targetAccountId' ],
24 unique: true
25 }
26 ]
27 }
28 )
29
30 const classMethods = [
31 associate
32 ]
33 addMethodsToModel(AccountFollow, classMethods)
34
35 return AccountFollow
36}
37
38// ------------------------------ STATICS ------------------------------
39
40function associate (models) {
41 AccountFollow.belongsTo(models.Account, {
42 foreignKey: {
43 name: 'accountId',
44 allowNull: false
45 },
46 onDelete: 'CASCADE'
47 })
48
49 AccountFollow.belongsTo(models.Account, {
50 foreignKey: {
51 name: 'targetAccountId',
52 allowNull: false
53 },
54 onDelete: 'CASCADE'
55 })
56}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
new file mode 100644
index 000000000..2ef3e2246
--- /dev/null
+++ b/server/models/account/account-interface.ts
@@ -0,0 +1,74 @@
1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird'
3
4import { PodInstance } from '../pod/pod-interface'
5import { VideoChannelInstance } from '../video/video-channel-interface'
6import { ActivityPubActor } from '../../../shared'
7import { ResultList } from '../../../shared/models/result-list.model'
8
9export namespace AccountMethods {
10 export type Load = (id: number) => Bluebird<AccountInstance>
11 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
12 export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
13 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
15 export type ListOwned = () => Bluebird<AccountInstance[]>
16 export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
17 export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> >
18
19 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
20 export type IsOwned = (this: AccountInstance) => boolean
21 export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
22 export type GetFollowingUrl = (this: AccountInstance) => string
23 export type GetFollowersUrl = (this: AccountInstance) => string
24 export type GetPublicKeyUrl = (this: AccountInstance) => string
25}
26
27export interface AccountClass {
28 loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
29 load: AccountMethods.Load
30 loadByUUID: AccountMethods.LoadByUUID
31 loadByUrl: AccountMethods.LoadByUrl
32 loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
33 listOwned: AccountMethods.ListOwned
34 listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
35 listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
36}
37
38export interface AccountAttributes {
39 name: string
40 url: string
41 publicKey: string
42 privateKey: string
43 followersCount: number
44 followingCount: number
45 inboxUrl: string
46 outboxUrl: string
47 sharedInboxUrl: string
48 followersUrl: string
49 followingUrl: string
50
51 uuid?: string
52
53 podId?: number
54 userId?: number
55 applicationId?: number
56}
57
58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
59 isOwned: AccountMethods.IsOwned
60 toActivityPubObject: AccountMethods.ToActivityPubObject
61 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
62 getFollowingUrl: AccountMethods.GetFollowingUrl
63 getFollowersUrl: AccountMethods.GetFollowersUrl
64 getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
65
66 id: number
67 createdAt: Date
68 updatedAt: Date
69
70 Pod: PodInstance
71 VideoChannels: VideoChannelInstance[]
72}
73
74export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts
new file mode 100644
index 000000000..82cbe38cc
--- /dev/null
+++ b/server/models/account/account-video-rate-interface.ts
@@ -0,0 +1,26 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace AccountVideoRateMethods {
7 export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
8}
9
10export interface AccountVideoRateClass {
11 load: AccountVideoRateMethods.Load
12}
13
14export interface AccountVideoRateAttributes {
15 type: VideoRateType
16 accountId: number
17 videoId: number
18}
19
20export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
21 id: number
22 createdAt: Date
23 updatedAt: Date
24}
25
26export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}
diff --git a/server/models/user/user-video-rate.ts b/server/models/account/account-video-rate.ts
index 7d6dd7281..7f7c97606 100644
--- a/server/models/user/user-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,5 +1,5 @@
1/* 1/*
2 User rates per video. 2 Account rates per video.
3*/ 3*/
4import { values } from 'lodash' 4import { values } from 'lodash'
5import * as Sequelize from 'sequelize' 5import * as Sequelize from 'sequelize'
@@ -8,17 +8,17 @@ import { VIDEO_RATE_TYPES } from '../../initializers'
8 8
9import { addMethodsToModel } from '../utils' 9import { addMethodsToModel } from '../utils'
10import { 10import {
11 UserVideoRateInstance, 11 AccountVideoRateInstance,
12 UserVideoRateAttributes, 12 AccountVideoRateAttributes,
13 13
14 UserVideoRateMethods 14 AccountVideoRateMethods
15} from './user-video-rate-interface' 15} from './account-video-rate-interface'
16 16
17let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> 17let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
18let load: UserVideoRateMethods.Load 18let load: AccountVideoRateMethods.Load
19 19
20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
21 UserVideoRate = sequelize.define<UserVideoRateInstance, UserVideoRateAttributes>('UserVideoRate', 21 AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
22 { 22 {
23 type: { 23 type: {
24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)), 24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
@@ -28,7 +28,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
28 { 28 {
29 indexes: [ 29 indexes: [
30 { 30 {
31 fields: [ 'videoId', 'userId', 'type' ], 31 fields: [ 'videoId', 'accountId', 'type' ],
32 unique: true 32 unique: true
33 } 33 }
34 ] 34 ]
@@ -40,15 +40,15 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
40 40
41 load 41 load
42 ] 42 ]
43 addMethodsToModel(UserVideoRate, classMethods) 43 addMethodsToModel(AccountVideoRate, classMethods)
44 44
45 return UserVideoRate 45 return AccountVideoRate
46} 46}
47 47
48// ------------------------------ STATICS ------------------------------ 48// ------------------------------ STATICS ------------------------------
49 49
50function associate (models) { 50function associate (models) {
51 UserVideoRate.belongsTo(models.Video, { 51 AccountVideoRate.belongsTo(models.Video, {
52 foreignKey: { 52 foreignKey: {
53 name: 'videoId', 53 name: 'videoId',
54 allowNull: false 54 allowNull: false
@@ -56,23 +56,23 @@ function associate (models) {
56 onDelete: 'CASCADE' 56 onDelete: 'CASCADE'
57 }) 57 })
58 58
59 UserVideoRate.belongsTo(models.User, { 59 AccountVideoRate.belongsTo(models.Account, {
60 foreignKey: { 60 foreignKey: {
61 name: 'userId', 61 name: 'accountId',
62 allowNull: false 62 allowNull: false
63 }, 63 },
64 onDelete: 'CASCADE' 64 onDelete: 'CASCADE'
65 }) 65 })
66} 66}
67 67
68load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) { 68load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) {
69 const options: Sequelize.FindOptions<UserVideoRateAttributes> = { 69 const options: Sequelize.FindOptions<AccountVideoRateAttributes> = {
70 where: { 70 where: {
71 userId, 71 accountId,
72 videoId 72 videoId
73 } 73 }
74 } 74 }
75 if (transaction) options.transaction = transaction 75 if (transaction) options.transaction = transaction
76 76
77 return UserVideoRate.findOne(options) 77 return AccountVideoRate.findOne(options)
78} 78}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
new file mode 100644
index 000000000..00c0aefd4
--- /dev/null
+++ b/server/models/account/account.ts
@@ -0,0 +1,444 @@
1import * as Sequelize from 'sequelize'
2
3import {
4 isUserUsernameValid,
5 isAccountPublicKeyValid,
6 isAccountUrlValid,
7 isAccountPrivateKeyValid,
8 isAccountFollowersCountValid,
9 isAccountFollowingCountValid,
10 isAccountInboxValid,
11 isAccountOutboxValid,
12 isAccountSharedInboxValid,
13 isAccountFollowersValid,
14 isAccountFollowingValid,
15 activityPubContextify
16} from '../../helpers'
17
18import { addMethodsToModel } from '../utils'
19import {
20 AccountInstance,
21 AccountAttributes,
22
23 AccountMethods
24} from './account-interface'
25
26let Account: Sequelize.Model<AccountInstance, AccountAttributes>
27let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
28let load: AccountMethods.Load
29let loadByUUID: AccountMethods.LoadByUUID
30let loadByUrl: AccountMethods.LoadByUrl
31let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
32let listOwned: AccountMethods.ListOwned
33let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
34let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
35let isOwned: AccountMethods.IsOwned
36let toActivityPubObject: AccountMethods.ToActivityPubObject
37let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
38let getFollowingUrl: AccountMethods.GetFollowingUrl
39let getFollowersUrl: AccountMethods.GetFollowersUrl
40let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
41
42export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
43 Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
44 {
45 uuid: {
46 type: DataTypes.UUID,
47 defaultValue: DataTypes.UUIDV4,
48 allowNull: false,
49 validate: {
50 isUUID: 4
51 }
52 },
53 name: {
54 type: DataTypes.STRING,
55 allowNull: false,
56 validate: {
57 usernameValid: value => {
58 const res = isUserUsernameValid(value)
59 if (res === false) throw new Error('Username is not valid.')
60 }
61 }
62 },
63 url: {
64 type: DataTypes.STRING,
65 allowNull: false,
66 validate: {
67 urlValid: value => {
68 const res = isAccountUrlValid(value)
69 if (res === false) throw new Error('URL is not valid.')
70 }
71 }
72 },
73 publicKey: {
74 type: DataTypes.STRING,
75 allowNull: false,
76 validate: {
77 publicKeyValid: value => {
78 const res = isAccountPublicKeyValid(value)
79 if (res === false) throw new Error('Public key is not valid.')
80 }
81 }
82 },
83 privateKey: {
84 type: DataTypes.STRING,
85 allowNull: false,
86 validate: {
87 privateKeyValid: value => {
88 const res = isAccountPrivateKeyValid(value)
89 if (res === false) throw new Error('Private key is not valid.')
90 }
91 }
92 },
93 followersCount: {
94 type: DataTypes.INTEGER,
95 allowNull: false,
96 validate: {
97 followersCountValid: value => {
98 const res = isAccountFollowersCountValid(value)
99 if (res === false) throw new Error('Followers count is not valid.')
100 }
101 }
102 },
103 followingCount: {
104 type: DataTypes.INTEGER,
105 allowNull: false,
106 validate: {
107 followersCountValid: value => {
108 const res = isAccountFollowingCountValid(value)
109 if (res === false) throw new Error('Following count is not valid.')
110 }
111 }
112 },
113 inboxUrl: {
114 type: DataTypes.STRING,
115 allowNull: false,
116 validate: {
117 inboxUrlValid: value => {
118 const res = isAccountInboxValid(value)
119 if (res === false) throw new Error('Inbox URL is not valid.')
120 }
121 }
122 },
123 outboxUrl: {
124 type: DataTypes.STRING,
125 allowNull: false,
126 validate: {
127 outboxUrlValid: value => {
128 const res = isAccountOutboxValid(value)
129 if (res === false) throw new Error('Outbox URL is not valid.')
130 }
131 }
132 },
133 sharedInboxUrl: {
134 type: DataTypes.STRING,
135 allowNull: false,
136 validate: {
137 sharedInboxUrlValid: value => {
138 const res = isAccountSharedInboxValid(value)
139 if (res === false) throw new Error('Shared inbox URL is not valid.')
140 }
141 }
142 },
143 followersUrl: {
144 type: DataTypes.STRING,
145 allowNull: false,
146 validate: {
147 followersUrlValid: value => {
148 const res = isAccountFollowersValid(value)
149 if (res === false) throw new Error('Followers URL is not valid.')
150 }
151 }
152 },
153 followingUrl: {
154 type: DataTypes.STRING,
155 allowNull: false,
156 validate: {
157 followingUrlValid: value => {
158 const res = isAccountFollowingValid(value)
159 if (res === false) throw new Error('Following URL is not valid.')
160 }
161 }
162 }
163 },
164 {
165 indexes: [
166 {
167 fields: [ 'name' ]
168 },
169 {
170 fields: [ 'podId' ]
171 },
172 {
173 fields: [ 'userId' ],
174 unique: true
175 },
176 {
177 fields: [ 'applicationId' ],
178 unique: true
179 },
180 {
181 fields: [ 'name', 'podId' ],
182 unique: true
183 }
184 ],
185 hooks: { afterDestroy }
186 }
187 )
188
189 const classMethods = [
190 associate,
191 loadAccountByPodAndUUID,
192 load,
193 loadByUUID,
194 loadLocalAccountByName,
195 listOwned,
196 listFollowerUrlsForApi,
197 listFollowingUrlsForApi
198 ]
199 const instanceMethods = [
200 isOwned,
201 toActivityPubObject,
202 getFollowerSharedInboxUrls,
203 getFollowingUrl,
204 getFollowersUrl,
205 getPublicKeyUrl
206 ]
207 addMethodsToModel(Account, classMethods, instanceMethods)
208
209 return Account
210}
211
212// ---------------------------------------------------------------------------
213
214function associate (models) {
215 Account.belongsTo(models.Pod, {
216 foreignKey: {
217 name: 'podId',
218 allowNull: true
219 },
220 onDelete: 'cascade'
221 })
222
223 Account.belongsTo(models.User, {
224 foreignKey: {
225 name: 'userId',
226 allowNull: true
227 },
228 onDelete: 'cascade'
229 })
230
231 Account.belongsTo(models.Application, {
232 foreignKey: {
233 name: 'userId',
234 allowNull: true
235 },
236 onDelete: 'cascade'
237 })
238
239 Account.hasMany(models.VideoChannel, {
240 foreignKey: {
241 name: 'accountId',
242 allowNull: false
243 },
244 onDelete: 'cascade',
245 hooks: true
246 })
247
248 Account.hasMany(models.AccountFollower, {
249 foreignKey: {
250 name: 'accountId',
251 allowNull: false
252 },
253 onDelete: 'cascade'
254 })
255
256 Account.hasMany(models.AccountFollower, {
257 foreignKey: {
258 name: 'targetAccountId',
259 allowNull: false
260 },
261 onDelete: 'cascade'
262 })
263}
264
265function afterDestroy (account: AccountInstance) {
266 if (account.isOwned()) {
267 const removeVideoAccountToFriendsParams = {
268 uuid: account.uuid
269 }
270
271 return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
272 }
273
274 return undefined
275}
276
277toActivityPubObject = function (this: AccountInstance) {
278 const type = this.podId ? 'Application' : 'Person'
279
280 const json = {
281 type,
282 id: this.url,
283 following: this.getFollowingUrl(),
284 followers: this.getFollowersUrl(),
285 inbox: this.inboxUrl,
286 outbox: this.outboxUrl,
287 preferredUsername: this.name,
288 url: this.url,
289 name: this.name,
290 endpoints: {
291 sharedInbox: this.sharedInboxUrl
292 },
293 uuid: this.uuid,
294 publicKey: {
295 id: this.getPublicKeyUrl(),
296 owner: this.url,
297 publicKeyPem: this.publicKey
298 }
299 }
300
301 return activityPubContextify(json)
302}
303
304isOwned = function (this: AccountInstance) {
305 return this.podId === null
306}
307
308getFollowerSharedInboxUrls = function (this: AccountInstance) {
309 const query: Sequelize.FindOptions<AccountAttributes> = {
310 attributes: [ 'sharedInboxUrl' ],
311 include: [
312 {
313 model: Account['sequelize'].models.AccountFollower,
314 where: {
315 targetAccountId: this.id
316 }
317 }
318 ]
319 }
320
321 return Account.findAll(query)
322 .then(accounts => accounts.map(a => a.sharedInboxUrl))
323}
324
325getFollowingUrl = function (this: AccountInstance) {
326 return this.url + '/followers'
327}
328
329getFollowersUrl = function (this: AccountInstance) {
330 return this.url + '/followers'
331}
332
333getPublicKeyUrl = function (this: AccountInstance) {
334 return this.url + '#main-key'
335}
336
337// ------------------------------ STATICS ------------------------------
338
339listOwned = function () {
340 const query: Sequelize.FindOptions<AccountAttributes> = {
341 where: {
342 podId: null
343 }
344 }
345
346 return Account.findAll(query)
347}
348
349listFollowerUrlsForApi = function (name: string, start: number, count: number) {
350 return createListFollowForApiQuery('followers', name, start, count)
351}
352
353listFollowingUrlsForApi = function (name: string, start: number, count: number) {
354 return createListFollowForApiQuery('following', name, start, count)
355}
356
357load = function (id: number) {
358 return Account.findById(id)
359}
360
361loadByUUID = function (uuid: string) {
362 const query: Sequelize.FindOptions<AccountAttributes> = {
363 where: {
364 uuid
365 }
366 }
367
368 return Account.findOne(query)
369}
370
371loadLocalAccountByName = function (name: string) {
372 const query: Sequelize.FindOptions<AccountAttributes> = {
373 where: {
374 name,
375 userId: {
376 [Sequelize.Op.ne]: null
377 }
378 }
379 }
380
381 return Account.findOne(query)
382}
383
384loadByUrl = function (url: string) {
385 const query: Sequelize.FindOptions<AccountAttributes> = {
386 where: {
387 url
388 }
389 }
390
391 return Account.findOne(query)
392}
393
394loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
395 const query: Sequelize.FindOptions<AccountAttributes> = {
396 where: {
397 podId,
398 uuid
399 },
400 transaction
401 }
402
403 return Account.find(query)
404}
405
406// ------------------------------ UTILS ------------------------------
407
408async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) {
409 let firstJoin: string
410 let secondJoin: string
411
412 if (type === 'followers') {
413 firstJoin = 'targetAccountId'
414 secondJoin = 'accountId'
415 } else {
416 firstJoin = 'accountId'
417 secondJoin = 'targetAccountId'
418 }
419
420 const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ]
421 const tasks: Promise<any>[] = []
422
423 for (const selection of selections) {
424 const query = 'SELECT ' + selection + ' FROM "Account" ' +
425 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
426 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' +
427 'WHERE "Account"."name" = \'$name\' ' +
428 'LIMIT ' + start + ', ' + count
429
430 const options = {
431 bind: { name },
432 type: Sequelize.QueryTypes.SELECT
433 }
434 tasks.push(Account['sequelize'].query(query, options))
435 }
436
437 const [ followers, [ { total } ]] = await Promise.all(tasks)
438 const urls: string[] = followers.map(f => f.url)
439
440 return {
441 data: urls,
442 total: parseInt(total, 10)
443 }
444}
diff --git a/server/models/account/index.ts b/server/models/account/index.ts
new file mode 100644
index 000000000..179f66974
--- /dev/null
+++ b/server/models/account/index.ts
@@ -0,0 +1,4 @@
1export * from './account-interface'
2export * from './account-follow-interface'
3export * from './account-video-rate-interface'
4export * from './user-interface'
diff --git a/server/models/user/user-interface.ts b/server/models/account/user-interface.ts
index 49c75aa3b..1a04fb750 100644
--- a/server/models/user/user-interface.ts
+++ b/server/models/account/user-interface.ts
@@ -1,10 +1,10 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4// Don't use barrel, import just what we need 4// Don't use barrel, import just what we need
5import { AccountInstance } from './account-interface'
5import { User as FormattedUser } from '../../../shared/models/users/user.model' 6import { User as FormattedUser } from '../../../shared/models/users/user.model'
6import { ResultList } from '../../../shared/models/result-list.model' 7import { ResultList } from '../../../shared/models/result-list.model'
7import { AuthorInstance } from '../video/author-interface'
8import { UserRight } from '../../../shared/models/users/user-right.enum' 8import { UserRight } from '../../../shared/models/users/user-right.enum'
9import { UserRole } from '../../../shared/models/users/user-role' 9import { UserRole } from '../../../shared/models/users/user-role'
10 10
@@ -15,18 +15,18 @@ export namespace UserMethods {
15 export type ToFormattedJSON = (this: UserInstance) => FormattedUser 15 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
16 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean> 16 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
17 17
18 export type CountTotal = () => Promise<number> 18 export type CountTotal = () => Bluebird<number>
19 19
20 export type GetByUsername = (username: string) => Promise<UserInstance> 20 export type GetByUsername = (username: string) => Bluebird<UserInstance>
21 21
22 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<UserInstance> > 22 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
23 23
24 export type LoadById = (id: number) => Promise<UserInstance> 24 export type LoadById = (id: number) => Bluebird<UserInstance>
25 25
26 export type LoadByUsername = (username: string) => Promise<UserInstance> 26 export type LoadByUsername = (username: string) => Bluebird<UserInstance>
27 export type LoadByUsernameAndPopulateChannels = (username: string) => Promise<UserInstance> 27 export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
28 28
29 export type LoadByUsernameOrEmail = (username: string, email: string) => Promise<UserInstance> 29 export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
30} 30}
31 31
32export interface UserClass { 32export interface UserClass {
@@ -53,7 +53,7 @@ export interface UserAttributes {
53 role: UserRole 53 role: UserRole
54 videoQuota: number 54 videoQuota: number
55 55
56 Author?: AuthorInstance 56 Account?: AccountInstance
57} 57}
58 58
59export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { 59export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
diff --git a/server/models/user/user.ts b/server/models/account/user.ts
index b974418d4..1401762c5 100644
--- a/server/models/user/user.ts
+++ b/server/models/account/user.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3 2
4import { getSort, addMethodsToModel } from '../utils' 3import { getSort, addMethodsToModel } from '../utils'
5import { 4import {
@@ -166,13 +165,13 @@ toFormattedJSON = function (this: UserInstance) {
166 videoQuota: this.videoQuota, 165 videoQuota: this.videoQuota,
167 createdAt: this.createdAt, 166 createdAt: this.createdAt,
168 author: { 167 author: {
169 id: this.Author.id, 168 id: this.Account.id,
170 uuid: this.Author.uuid 169 uuid: this.Account.uuid
171 } 170 }
172 } 171 }
173 172
174 if (Array.isArray(this.Author.VideoChannels) === true) { 173 if (Array.isArray(this.Account.VideoChannels) === true) {
175 const videoChannels = this.Author.VideoChannels 174 const videoChannels = this.Account.VideoChannels
176 .map(c => c.toFormattedJSON()) 175 .map(c => c.toFormattedJSON())
177 .sort((v1, v2) => { 176 .sort((v1, v2) => {
178 if (v1.createdAt < v2.createdAt) return -1 177 if (v1.createdAt < v2.createdAt) return -1
@@ -198,7 +197,7 @@ isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.Fi
198// ------------------------------ STATICS ------------------------------ 197// ------------------------------ STATICS ------------------------------
199 198
200function associate (models) { 199function associate (models) {
201 User.hasOne(models.Author, { 200 User.hasOne(models.Account, {
202 foreignKey: 'userId', 201 foreignKey: 'userId',
203 onDelete: 'cascade' 202 onDelete: 'cascade'
204 }) 203 })
@@ -218,7 +217,7 @@ getByUsername = function (username: string) {
218 where: { 217 where: {
219 username: username 218 username: username
220 }, 219 },
221 include: [ { model: User['sequelize'].models.Author, required: true } ] 220 include: [ { model: User['sequelize'].models.Account, required: true } ]
222 } 221 }
223 222
224 return User.findOne(query) 223 return User.findOne(query)
@@ -229,7 +228,7 @@ listForApi = function (start: number, count: number, sort: string) {
229 offset: start, 228 offset: start,
230 limit: count, 229 limit: count,
231 order: [ getSort(sort) ], 230 order: [ getSort(sort) ],
232 include: [ { model: User['sequelize'].models.Author, required: true } ] 231 include: [ { model: User['sequelize'].models.Account, required: true } ]
233 } 232 }
234 233
235 return User.findAndCountAll(query).then(({ rows, count }) => { 234 return User.findAndCountAll(query).then(({ rows, count }) => {
@@ -242,7 +241,7 @@ listForApi = function (start: number, count: number, sort: string) {
242 241
243loadById = function (id: number) { 242loadById = function (id: number) {
244 const options = { 243 const options = {
245 include: [ { model: User['sequelize'].models.Author, required: true } ] 244 include: [ { model: User['sequelize'].models.Account, required: true } ]
246 } 245 }
247 246
248 return User.findById(id, options) 247 return User.findById(id, options)
@@ -253,7 +252,7 @@ loadByUsername = function (username: string) {
253 where: { 252 where: {
254 username 253 username
255 }, 254 },
256 include: [ { model: User['sequelize'].models.Author, required: true } ] 255 include: [ { model: User['sequelize'].models.Account, required: true } ]
257 } 256 }
258 257
259 return User.findOne(query) 258 return User.findOne(query)
@@ -266,7 +265,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
266 }, 265 },
267 include: [ 266 include: [
268 { 267 {
269 model: User['sequelize'].models.Author, 268 model: User['sequelize'].models.Account,
270 required: true, 269 required: true,
271 include: [ User['sequelize'].models.VideoChannel ] 270 include: [ User['sequelize'].models.VideoChannel ]
272 } 271 }
@@ -278,7 +277,7 @@ loadByUsernameAndPopulateChannels = function (username: string) {
278 277
279loadByUsernameOrEmail = function (username: string, email: string) { 278loadByUsernameOrEmail = function (username: string, email: string) {
280 const query = { 279 const query = {
281 include: [ { model: User['sequelize'].models.Author, required: true } ], 280 include: [ { model: User['sequelize'].models.Account, required: true } ],
282 where: { 281 where: {
283 [Sequelize.Op.or]: [ { username }, { email } ] 282 [Sequelize.Op.or]: [ { username }, { email } ]
284 } 283 }
@@ -296,8 +295,8 @@ function getOriginalVideoFileTotalFromUser (user: UserInstance) {
296 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + 295 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
297 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + 296 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
298 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + 297 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
299 'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' + 298 'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' +
300 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + 299 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
301 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' 300 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
302 301
303 const options = { 302 const options = {
diff --git a/server/models/index.ts b/server/models/index.ts
index b392a8a77..29479e067 100644
--- a/server/models/index.ts
+++ b/server/models/index.ts
@@ -3,5 +3,5 @@ export * from './job'
3export * from './oauth' 3export * from './oauth'
4export * from './pod' 4export * from './pod'
5export * from './request' 5export * from './request'
6export * from './user' 6export * from './account'
7export * from './video' 7export * from './video'
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts
index ba5622977..163930a4f 100644
--- a/server/models/job/job-interface.ts
+++ b/server/models/job/job-interface.ts
@@ -1,14 +1,14 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { JobState } from '../../../shared/models/job.model' 4import { JobCategory, JobState } from '../../../shared/models/job.model'
5 5
6export namespace JobMethods { 6export namespace JobMethods {
7 export type ListWithLimit = (limit: number, state: JobState) => Promise<JobInstance[]> 7 export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
8} 8}
9 9
10export interface JobClass { 10export interface JobClass {
11 listWithLimit: JobMethods.ListWithLimit 11 listWithLimitByCategory: JobMethods.ListWithLimitByCategory
12} 12}
13 13
14export interface JobAttributes { 14export interface JobAttributes {
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index 968f9d71d..ce1203e5a 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -1,7 +1,7 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3 3
4import { JOB_STATES } from '../../initializers' 4import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
5 5
6import { addMethodsToModel } from '../utils' 6import { addMethodsToModel } from '../utils'
7import { 7import {
@@ -13,7 +13,7 @@ import {
13import { JobState } from '../../../shared/models/job.model' 13import { JobState } from '../../../shared/models/job.model'
14 14
15let Job: Sequelize.Model<JobInstance, JobAttributes> 15let Job: Sequelize.Model<JobInstance, JobAttributes>
16let listWithLimit: JobMethods.ListWithLimit 16let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
17 17
18export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 18export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
19 Job = sequelize.define<JobInstance, JobAttributes>('Job', 19 Job = sequelize.define<JobInstance, JobAttributes>('Job',
@@ -22,6 +22,10 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
22 type: DataTypes.ENUM(values(JOB_STATES)), 22 type: DataTypes.ENUM(values(JOB_STATES)),
23 allowNull: false 23 allowNull: false
24 }, 24 },
25 category: {
26 type: DataTypes.ENUM(values(JOB_CATEGORIES)),
27 allowNull: false
28 },
25 handlerName: { 29 handlerName: {
26 type: DataTypes.STRING, 30 type: DataTypes.STRING,
27 allowNull: false 31 allowNull: false
@@ -40,7 +44,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
40 } 44 }
41 ) 45 )
42 46
43 const classMethods = [ listWithLimit ] 47 const classMethods = [ listWithLimitByCategory ]
44 addMethodsToModel(Job, classMethods) 48 addMethodsToModel(Job, classMethods)
45 49
46 return Job 50 return Job
@@ -48,7 +52,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
48 52
49// --------------------------------------------------------------------------- 53// ---------------------------------------------------------------------------
50 54
51listWithLimit = function (limit: number, state: JobState) { 55listWithLimitByCategory = function (limit: number, state: JobState) {
52 const query = { 56 const query = {
53 order: [ 57 order: [
54 [ 'id', 'ASC' ] 58 [ 'id', 'ASC' ]
diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts
index 0c947bde8..ef97893c4 100644
--- a/server/models/oauth/oauth-token-interface.ts
+++ b/server/models/oauth/oauth-token-interface.ts
@@ -1,7 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { UserModel } from '../user/user-interface' 4import { UserModel } from '../account/user-interface'
5 5
6export type OAuthTokenInfo = { 6export type OAuthTokenInfo = {
7 refreshToken: string 7 refreshToken: string
diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts
index 7e095d424..6c5aab3fa 100644
--- a/server/models/pod/pod-interface.ts
+++ b/server/models/pod/pod-interface.ts
@@ -48,9 +48,7 @@ export interface PodClass {
48export interface PodAttributes { 48export interface PodAttributes {
49 id?: number 49 id?: number
50 host?: string 50 host?: string
51 publicKey?: string
52 score?: number | Sequelize.literal // Sequelize literal for 'score +' + value 51 score?: number | Sequelize.literal // Sequelize literal for 'score +' + value
53 email?: string
54} 52}
55 53
56export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> { 54export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> {
diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts
index 6b33336b8..7c8b49bf8 100644
--- a/server/models/pod/pod.ts
+++ b/server/models/pod/pod.ts
@@ -39,10 +39,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
39 } 39 }
40 } 40 }
41 }, 41 },
42 publicKey: {
43 type: DataTypes.STRING(5000),
44 allowNull: false
45 },
46 score: { 42 score: {
47 type: DataTypes.INTEGER, 43 type: DataTypes.INTEGER,
48 defaultValue: FRIEND_SCORE.BASE, 44 defaultValue: FRIEND_SCORE.BASE,
@@ -51,13 +47,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
51 isInt: true, 47 isInt: true,
52 max: FRIEND_SCORE.MAX 48 max: FRIEND_SCORE.MAX
53 } 49 }
54 },
55 email: {
56 type: DataTypes.STRING(400),
57 allowNull: false,
58 validate: {
59 isEmail: true
60 }
61 } 50 }
62 }, 51 },
63 { 52 {
@@ -100,7 +89,6 @@ toFormattedJSON = function (this: PodInstance) {
100 const json = { 89 const json = {
101 id: this.id, 90 id: this.id,
102 host: this.host, 91 host: this.host,
103 email: this.email,
104 score: this.score as number, 92 score: this.score as number,
105 createdAt: this.createdAt 93 createdAt: this.createdAt
106 } 94 }
diff --git a/server/models/user/index.ts b/server/models/user/index.ts
deleted file mode 100644
index ed3689518..000000000
--- a/server/models/user/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './user-video-rate-interface'
2export * from './user-interface'
diff --git a/server/models/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts
deleted file mode 100644
index ea0fdc4d9..000000000
--- a/server/models/user/user-video-rate-interface.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5
6export namespace UserVideoRateMethods {
7 export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance>
8}
9
10export interface UserVideoRateClass {
11 load: UserVideoRateMethods.Load
12}
13
14export interface UserVideoRateAttributes {
15 type: VideoRateType
16 userId: number
17 videoId: number
18}
19
20export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance<UserVideoRateAttributes> {
21 id: number
22 createdAt: Date
23 updatedAt: Date
24}
25
26export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> {}
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts
deleted file mode 100644
index fc69ff3c2..000000000
--- a/server/models/video/author-interface.ts
+++ /dev/null
@@ -1,45 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { PodInstance } from '../pod/pod-interface'
5import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model'
6import { VideoChannelInstance } from './video-channel-interface'
7
8export namespace AuthorMethods {
9 export type Load = (id: number) => Promise<AuthorInstance>
10 export type LoadByUUID = (uuid: string) => Promise<AuthorInstance>
11 export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise<AuthorInstance>
12 export type ListOwned = () => Promise<AuthorInstance[]>
13
14 export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData
15 export type IsOwned = (this: AuthorInstance) => boolean
16}
17
18export interface AuthorClass {
19 loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
20 load: AuthorMethods.Load
21 loadByUUID: AuthorMethods.LoadByUUID
22 listOwned: AuthorMethods.ListOwned
23}
24
25export interface AuthorAttributes {
26 name: string
27 uuid?: string
28
29 podId?: number
30 userId?: number
31}
32
33export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance<AuthorAttributes> {
34 isOwned: AuthorMethods.IsOwned
35 toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
36
37 id: number
38 createdAt: Date
39 updatedAt: Date
40
41 Pod: PodInstance
42 VideoChannels: VideoChannelInstance[]
43}
44
45export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {}
diff --git a/server/models/video/author.ts b/server/models/video/author.ts
deleted file mode 100644
index 43f84c3ea..000000000
--- a/server/models/video/author.ts
+++ /dev/null
@@ -1,171 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3import { isUserUsernameValid } from '../../helpers'
4import { removeVideoAuthorToFriends } from '../../lib'
5
6import { addMethodsToModel } from '../utils'
7import {
8 AuthorInstance,
9 AuthorAttributes,
10
11 AuthorMethods
12} from './author-interface'
13
14let Author: Sequelize.Model<AuthorInstance, AuthorAttributes>
15let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID
16let load: AuthorMethods.Load
17let loadByUUID: AuthorMethods.LoadByUUID
18let listOwned: AuthorMethods.ListOwned
19let isOwned: AuthorMethods.IsOwned
20let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON
21
22export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
23 Author = sequelize.define<AuthorInstance, AuthorAttributes>('Author',
24 {
25 uuid: {
26 type: DataTypes.UUID,
27 defaultValue: DataTypes.UUIDV4,
28 allowNull: false,
29 validate: {
30 isUUID: 4
31 }
32 },
33 name: {
34 type: DataTypes.STRING,
35 allowNull: false,
36 validate: {
37 usernameValid: value => {
38 const res = isUserUsernameValid(value)
39 if (res === false) throw new Error('Username is not valid.')
40 }
41 }
42 }
43 },
44 {
45 indexes: [
46 {
47 fields: [ 'name' ]
48 },
49 {
50 fields: [ 'podId' ]
51 },
52 {
53 fields: [ 'userId' ],
54 unique: true
55 },
56 {
57 fields: [ 'name', 'podId' ],
58 unique: true
59 }
60 ],
61 hooks: { afterDestroy }
62 }
63 )
64
65 const classMethods = [
66 associate,
67 loadAuthorByPodAndUUID,
68 load,
69 loadByUUID,
70 listOwned
71 ]
72 const instanceMethods = [
73 isOwned,
74 toAddRemoteJSON
75 ]
76 addMethodsToModel(Author, classMethods, instanceMethods)
77
78 return Author
79}
80
81// ---------------------------------------------------------------------------
82
83function associate (models) {
84 Author.belongsTo(models.Pod, {
85 foreignKey: {
86 name: 'podId',
87 allowNull: true
88 },
89 onDelete: 'cascade'
90 })
91
92 Author.belongsTo(models.User, {
93 foreignKey: {
94 name: 'userId',
95 allowNull: true
96 },
97 onDelete: 'cascade'
98 })
99
100 Author.hasMany(models.VideoChannel, {
101 foreignKey: {
102 name: 'authorId',
103 allowNull: false
104 },
105 onDelete: 'cascade',
106 hooks: true
107 })
108}
109
110function afterDestroy (author: AuthorInstance) {
111 if (author.isOwned()) {
112 const removeVideoAuthorToFriendsParams = {
113 uuid: author.uuid
114 }
115
116 return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams)
117 }
118
119 return undefined
120}
121
122toAddRemoteJSON = function (this: AuthorInstance) {
123 const json = {
124 uuid: this.uuid,
125 name: this.name
126 }
127
128 return json
129}
130
131isOwned = function (this: AuthorInstance) {
132 return this.podId === null
133}
134
135// ------------------------------ STATICS ------------------------------
136
137listOwned = function () {
138 const query: Sequelize.FindOptions<AuthorAttributes> = {
139 where: {
140 podId: null
141 }
142 }
143
144 return Author.findAll(query)
145}
146
147load = function (id: number) {
148 return Author.findById(id)
149}
150
151loadByUUID = function (uuid: string) {
152 const query: Sequelize.FindOptions<AuthorAttributes> = {
153 where: {
154 uuid
155 }
156 }
157
158 return Author.findOne(query)
159}
160
161loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) {
162 const query: Sequelize.FindOptions<AuthorAttributes> = {
163 where: {
164 podId,
165 uuid
166 },
167 transaction
168 }
169
170 return Author.find(query)
171}
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts
index b8d3e0f42..477f97cd4 100644
--- a/server/models/video/video-channel-interface.ts
+++ b/server/models/video/video-channel-interface.ts
@@ -1,42 +1,42 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' 4import { ResultList } from '../../../shared'
5 5
6// Don't use barrel, import just what we need 6// Don't use barrel, import just what we need
7import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' 7import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
8import { AuthorInstance } from './author-interface'
9import { VideoInstance } from './video-interface' 8import { VideoInstance } from './video-interface'
9import { AccountInstance } from '../account/account-interface'
10import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
10 11
11export namespace VideoChannelMethods { 12export namespace VideoChannelMethods {
12 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel 13 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
13 export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData 14 export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject
14 export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData
15 export type IsOwned = (this: VideoChannelInstance) => boolean 15 export type IsOwned = (this: VideoChannelInstance) => boolean
16 16
17 export type CountByAuthor = (authorId: number) => Promise<number> 17 export type CountByAccount = (accountId: number) => Promise<number>
18 export type ListOwned = () => Promise<VideoChannelInstance[]> 18 export type ListOwned = () => Promise<VideoChannelInstance[]>
19 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> > 19 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
20 export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise<VideoChannelInstance> 20 export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance>
21 export type ListByAuthor = (authorId: number) => Promise< ResultList<VideoChannelInstance> > 21 export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> >
22 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoChannelInstance> 22 export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance>
23 export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise<VideoChannelInstance> 23 export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance>
24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
26 export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise<VideoChannelInstance> 26 export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
27} 27}
28 28
29export interface VideoChannelClass { 29export interface VideoChannelClass {
30 countByAuthor: VideoChannelMethods.CountByAuthor 30 countByAccount: VideoChannelMethods.CountByAccount
31 listForApi: VideoChannelMethods.ListForApi 31 listForApi: VideoChannelMethods.ListForApi
32 listByAuthor: VideoChannelMethods.ListByAuthor 32 listByAccount: VideoChannelMethods.ListByAccount
33 listOwned: VideoChannelMethods.ListOwned 33 listOwned: VideoChannelMethods.ListOwned
34 loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor 34 loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
35 loadByUUID: VideoChannelMethods.LoadByUUID 35 loadByUUID: VideoChannelMethods.LoadByUUID
36 loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 36 loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
37 loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor 37 loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
38 loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor 38 loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
39 loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos 39 loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
40} 40}
41 41
42export interface VideoChannelAttributes { 42export interface VideoChannelAttributes {
@@ -45,8 +45,9 @@ export interface VideoChannelAttributes {
45 name: string 45 name: string
46 description: string 46 description: string
47 remote: boolean 47 remote: boolean
48 url: string
48 49
49 Author?: AuthorInstance 50 Account?: AccountInstance
50 Videos?: VideoInstance[] 51 Videos?: VideoInstance[]
51} 52}
52 53
@@ -57,8 +58,7 @@ export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAtt
57 58
58 isOwned: VideoChannelMethods.IsOwned 59 isOwned: VideoChannelMethods.IsOwned
59 toFormattedJSON: VideoChannelMethods.ToFormattedJSON 60 toFormattedJSON: VideoChannelMethods.ToFormattedJSON
60 toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON 61 toActivityPubObject: VideoChannelMethods.ToActivityPubObject
61 toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
62} 62}
63 63
64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {} 64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 46c2db63f..c17828f3e 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -13,19 +13,18 @@ import {
13 13
14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> 14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON 15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
16let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON 16let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
17let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON
18let isOwned: VideoChannelMethods.IsOwned 17let isOwned: VideoChannelMethods.IsOwned
19let countByAuthor: VideoChannelMethods.CountByAuthor 18let countByAccount: VideoChannelMethods.CountByAccount
20let listOwned: VideoChannelMethods.ListOwned 19let listOwned: VideoChannelMethods.ListOwned
21let listForApi: VideoChannelMethods.ListForApi 20let listForApi: VideoChannelMethods.ListForApi
22let listByAuthor: VideoChannelMethods.ListByAuthor 21let listByAccount: VideoChannelMethods.ListByAccount
23let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor 22let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
24let loadByUUID: VideoChannelMethods.LoadByUUID 23let loadByUUID: VideoChannelMethods.LoadByUUID
25let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor 24let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
26let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor 25let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
27let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 26let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
28let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos 27let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
29 28
30export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 29export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
31 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', 30 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
@@ -62,12 +61,19 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
62 type: DataTypes.BOOLEAN, 61 type: DataTypes.BOOLEAN,
63 allowNull: false, 62 allowNull: false,
64 defaultValue: false 63 defaultValue: false
64 },
65 url: {
66 type: DataTypes.STRING,
67 allowNull: false,
68 validate: {
69 isUrl: true
70 }
65 } 71 }
66 }, 72 },
67 { 73 {
68 indexes: [ 74 indexes: [
69 { 75 {
70 fields: [ 'authorId' ] 76 fields: [ 'accountId' ]
71 } 77 }
72 ], 78 ],
73 hooks: { 79 hooks: {
@@ -80,21 +86,20 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
80 associate, 86 associate,
81 87
82 listForApi, 88 listForApi,
83 listByAuthor, 89 listByAccount,
84 listOwned, 90 listOwned,
85 loadByIdAndAuthor, 91 loadByIdAndAccount,
86 loadAndPopulateAuthor, 92 loadAndPopulateAccount,
87 loadByUUIDAndPopulateAuthor, 93 loadByUUIDAndPopulateAccount,
88 loadByUUID, 94 loadByUUID,
89 loadByHostAndUUID, 95 loadByHostAndUUID,
90 loadAndPopulateAuthorAndVideos, 96 loadAndPopulateAccountAndVideos,
91 countByAuthor 97 countByAccount
92 ] 98 ]
93 const instanceMethods = [ 99 const instanceMethods = [
94 isOwned, 100 isOwned,
95 toFormattedJSON, 101 toFormattedJSON,
96 toAddRemoteJSON, 102 toActivityPubObject,
97 toUpdateRemoteJSON
98 ] 103 ]
99 addMethodsToModel(VideoChannel, classMethods, instanceMethods) 104 addMethodsToModel(VideoChannel, classMethods, instanceMethods)
100 105
@@ -118,10 +123,10 @@ toFormattedJSON = function (this: VideoChannelInstance) {
118 updatedAt: this.updatedAt 123 updatedAt: this.updatedAt
119 } 124 }
120 125
121 if (this.Author !== undefined) { 126 if (this.Account !== undefined) {
122 json['owner'] = { 127 json['owner'] = {
123 name: this.Author.name, 128 name: this.Account.name,
124 uuid: this.Author.uuid 129 uuid: this.Account.uuid
125 } 130 }
126 } 131 }
127 132
@@ -132,27 +137,14 @@ toFormattedJSON = function (this: VideoChannelInstance) {
132 return json 137 return json
133} 138}
134 139
135toAddRemoteJSON = function (this: VideoChannelInstance) { 140toActivityPubObject = function (this: VideoChannelInstance) {
136 const json = {
137 uuid: this.uuid,
138 name: this.name,
139 description: this.description,
140 createdAt: this.createdAt,
141 updatedAt: this.updatedAt,
142 ownerUUID: this.Author.uuid
143 }
144
145 return json
146}
147
148toUpdateRemoteJSON = function (this: VideoChannelInstance) {
149 const json = { 141 const json = {
150 uuid: this.uuid, 142 uuid: this.uuid,
151 name: this.name, 143 name: this.name,
152 description: this.description, 144 description: this.description,
153 createdAt: this.createdAt, 145 createdAt: this.createdAt,
154 updatedAt: this.updatedAt, 146 updatedAt: this.updatedAt,
155 ownerUUID: this.Author.uuid 147 ownerUUID: this.Account.uuid
156 } 148 }
157 149
158 return json 150 return json
@@ -161,9 +153,9 @@ toUpdateRemoteJSON = function (this: VideoChannelInstance) {
161// ------------------------------ STATICS ------------------------------ 153// ------------------------------ STATICS ------------------------------
162 154
163function associate (models) { 155function associate (models) {
164 VideoChannel.belongsTo(models.Author, { 156 VideoChannel.belongsTo(models.Account, {
165 foreignKey: { 157 foreignKey: {
166 name: 'authorId', 158 name: 'accountId',
167 allowNull: false 159 allowNull: false
168 }, 160 },
169 onDelete: 'CASCADE' 161 onDelete: 'CASCADE'
@@ -190,10 +182,10 @@ function afterDestroy (videoChannel: VideoChannelInstance) {
190 return undefined 182 return undefined
191} 183}
192 184
193countByAuthor = function (authorId: number) { 185countByAccount = function (accountId: number) {
194 const query = { 186 const query = {
195 where: { 187 where: {
196 authorId 188 accountId
197 } 189 }
198 } 190 }
199 191
@@ -205,7 +197,7 @@ listOwned = function () {
205 where: { 197 where: {
206 remote: false 198 remote: false
207 }, 199 },
208 include: [ VideoChannel['sequelize'].models.Author ] 200 include: [ VideoChannel['sequelize'].models.Account ]
209 } 201 }
210 202
211 return VideoChannel.findAll(query) 203 return VideoChannel.findAll(query)
@@ -218,7 +210,7 @@ listForApi = function (start: number, count: number, sort: string) {
218 order: [ getSort(sort) ], 210 order: [ getSort(sort) ],
219 include: [ 211 include: [
220 { 212 {
221 model: VideoChannel['sequelize'].models.Author, 213 model: VideoChannel['sequelize'].models.Account,
222 required: true, 214 required: true,
223 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 215 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
224 } 216 }
@@ -230,14 +222,14 @@ listForApi = function (start: number, count: number, sort: string) {
230 }) 222 })
231} 223}
232 224
233listByAuthor = function (authorId: number) { 225listByAccount = function (accountId: number) {
234 const query = { 226 const query = {
235 order: [ getSort('createdAt') ], 227 order: [ getSort('createdAt') ],
236 include: [ 228 include: [
237 { 229 {
238 model: VideoChannel['sequelize'].models.Author, 230 model: VideoChannel['sequelize'].models.Account,
239 where: { 231 where: {
240 id: authorId 232 id: accountId
241 }, 233 },
242 required: true, 234 required: true,
243 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 235 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
@@ -269,7 +261,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
269 }, 261 },
270 include: [ 262 include: [
271 { 263 {
272 model: VideoChannel['sequelize'].models.Author, 264 model: VideoChannel['sequelize'].models.Account,
273 include: [ 265 include: [
274 { 266 {
275 model: VideoChannel['sequelize'].models.Pod, 267 model: VideoChannel['sequelize'].models.Pod,
@@ -288,15 +280,15 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
288 return VideoChannel.findOne(query) 280 return VideoChannel.findOne(query)
289} 281}
290 282
291loadByIdAndAuthor = function (id: number, authorId: number) { 283loadByIdAndAccount = function (id: number, accountId: number) {
292 const options = { 284 const options = {
293 where: { 285 where: {
294 id, 286 id,
295 authorId 287 accountId
296 }, 288 },
297 include: [ 289 include: [
298 { 290 {
299 model: VideoChannel['sequelize'].models.Author, 291 model: VideoChannel['sequelize'].models.Account,
300 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 292 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
301 } 293 }
302 ] 294 ]
@@ -305,11 +297,11 @@ loadByIdAndAuthor = function (id: number, authorId: number) {
305 return VideoChannel.findOne(options) 297 return VideoChannel.findOne(options)
306} 298}
307 299
308loadAndPopulateAuthor = function (id: number) { 300loadAndPopulateAccount = function (id: number) {
309 const options = { 301 const options = {
310 include: [ 302 include: [
311 { 303 {
312 model: VideoChannel['sequelize'].models.Author, 304 model: VideoChannel['sequelize'].models.Account,
313 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 305 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
314 } 306 }
315 ] 307 ]
@@ -318,14 +310,14 @@ loadAndPopulateAuthor = function (id: number) {
318 return VideoChannel.findById(id, options) 310 return VideoChannel.findById(id, options)
319} 311}
320 312
321loadByUUIDAndPopulateAuthor = function (uuid: string) { 313loadByUUIDAndPopulateAccount = function (uuid: string) {
322 const options = { 314 const options = {
323 where: { 315 where: {
324 uuid 316 uuid
325 }, 317 },
326 include: [ 318 include: [
327 { 319 {
328 model: VideoChannel['sequelize'].models.Author, 320 model: VideoChannel['sequelize'].models.Account,
329 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 321 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
330 } 322 }
331 ] 323 ]
@@ -334,11 +326,11 @@ loadByUUIDAndPopulateAuthor = function (uuid: string) {
334 return VideoChannel.findOne(options) 326 return VideoChannel.findOne(options)
335} 327}
336 328
337loadAndPopulateAuthorAndVideos = function (id: number) { 329loadAndPopulateAccountAndVideos = function (id: number) {
338 const options = { 330 const options = {
339 include: [ 331 include: [
340 { 332 {
341 model: VideoChannel['sequelize'].models.Author, 333 model: VideoChannel['sequelize'].models.Account,
342 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] 334 include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
343 }, 335 },
344 VideoChannel['sequelize'].models.Video 336 VideoChannel['sequelize'].models.Video
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index cfe65f9aa..e62e25a82 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -1,5 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4import { TagAttributes, TagInstance } from './tag-interface' 4import { TagAttributes, TagInstance } from './tag-interface'
5import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' 5import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
@@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/
13import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' 13import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
14import { ResultList } from '../../../shared/models/result-list.model' 14import { ResultList } from '../../../shared/models/result-list.model'
15import { VideoChannelInstance } from './video-channel-interface' 15import { VideoChannelInstance } from './video-channel-interface'
16import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
16 17
17export namespace VideoMethods { 18export namespace VideoMethods {
18 export type GetThumbnailName = (this: VideoInstance) => string 19 export type GetThumbnailName = (this: VideoInstance) => string
@@ -29,8 +30,7 @@ export namespace VideoMethods {
29 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string 30 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
30 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void> 31 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
31 32
32 export type ToAddRemoteJSON = (this: VideoInstance) => Promise<RemoteVideoCreateData> 33 export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject
33 export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData
34 34
35 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void> 35 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
36 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void> 36 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
@@ -40,31 +40,35 @@ export namespace VideoMethods {
40 export type GetPreviewPath = (this: VideoInstance) => string 40 export type GetPreviewPath = (this: VideoInstance) => string
41 export type GetDescriptionPath = (this: VideoInstance) => string 41 export type GetDescriptionPath = (this: VideoInstance) => string
42 export type GetTruncatedDescription = (this: VideoInstance) => string 42 export type GetTruncatedDescription = (this: VideoInstance) => string
43 export type GetCategoryLabel = (this: VideoInstance) => string
44 export type GetLicenceLabel = (this: VideoInstance) => string
45 export type GetLanguageLabel = (this: VideoInstance) => string
43 46
44 // Return thumbnail name 47 // Return thumbnail name
45 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 48 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
46 49
47 export type List = () => Promise<VideoInstance[]> 50 export type List = () => Bluebird<VideoInstance[]>
48 export type ListOwnedAndPopulateAuthorAndTags = () => Promise<VideoInstance[]> 51 export type ListOwnedAndPopulateAccountAndTags = () => Bluebird<VideoInstance[]>
49 export type ListOwnedByAuthor = (author: string) => Promise<VideoInstance[]> 52 export type ListOwnedByAccount = (account: string) => Bluebird<VideoInstance[]>
50 53
51 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > 54 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
52 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList<VideoInstance> > 55 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
53 export type SearchAndPopulateAuthorAndPodAndTags = ( 56 export type SearchAndPopulateAccountAndPodAndTags = (
54 value: string, 57 value: string,
55 field: string, 58 field: string,
56 start: number, 59 start: number,
57 count: number, 60 count: number,
58 sort: string 61 sort: string
59 ) => Promise< ResultList<VideoInstance> > 62 ) => Bluebird< ResultList<VideoInstance> >
60 63
61 export type Load = (id: number) => Promise<VideoInstance> 64 export type Load = (id: number) => Bluebird<VideoInstance>
62 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 65 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
63 export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 66 export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
64 export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise<VideoInstance> 67 export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
65 export type LoadAndPopulateAuthor = (id: number) => Promise<VideoInstance> 68 export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
66 export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise<VideoInstance> 69 export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
67 export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise<VideoInstance> 70 export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
71 export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
68 72
69 export type RemoveThumbnail = (this: VideoInstance) => Promise<void> 73 export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
70 export type RemovePreview = (this: VideoInstance) => Promise<void> 74 export type RemovePreview = (this: VideoInstance) => Promise<void>
@@ -77,16 +81,17 @@ export interface VideoClass {
77 list: VideoMethods.List 81 list: VideoMethods.List
78 listForApi: VideoMethods.ListForApi 82 listForApi: VideoMethods.ListForApi
79 listUserVideosForApi: VideoMethods.ListUserVideosForApi 83 listUserVideosForApi: VideoMethods.ListUserVideosForApi
80 listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 84 listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
81 listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 85 listOwnedByAccount: VideoMethods.ListOwnedByAccount
82 load: VideoMethods.Load 86 load: VideoMethods.Load
83 loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor 87 loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
84 loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 88 loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
85 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 89 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
86 loadByUUID: VideoMethods.LoadByUUID 90 loadByUUID: VideoMethods.LoadByUUID
91 loadByUrl: VideoMethods.LoadByUrl
87 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 92 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
88 loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 93 loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
89 searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 94 searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
90} 95}
91 96
92export interface VideoAttributes { 97export interface VideoAttributes {
@@ -104,7 +109,9 @@ export interface VideoAttributes {
104 likes?: number 109 likes?: number
105 dislikes?: number 110 dislikes?: number
106 remote: boolean 111 remote: boolean
112 url: string
107 113
114 parentId?: number
108 channelId?: number 115 channelId?: number
109 116
110 VideoChannel?: VideoChannelInstance 117 VideoChannel?: VideoChannelInstance
@@ -132,16 +139,18 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
132 removePreview: VideoMethods.RemovePreview 139 removePreview: VideoMethods.RemovePreview
133 removeThumbnail: VideoMethods.RemoveThumbnail 140 removeThumbnail: VideoMethods.RemoveThumbnail
134 removeTorrent: VideoMethods.RemoveTorrent 141 removeTorrent: VideoMethods.RemoveTorrent
135 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 142 toActivityPubObject: VideoMethods.ToActivityPubObject
136 toFormattedJSON: VideoMethods.ToFormattedJSON 143 toFormattedJSON: VideoMethods.ToFormattedJSON
137 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON 144 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
138 toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
139 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 145 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
140 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 146 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
141 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 147 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
142 getEmbedPath: VideoMethods.GetEmbedPath 148 getEmbedPath: VideoMethods.GetEmbedPath
143 getDescriptionPath: VideoMethods.GetDescriptionPath 149 getDescriptionPath: VideoMethods.GetDescriptionPath
144 getTruncatedDescription: VideoMethods.GetTruncatedDescription 150 getTruncatedDescription: VideoMethods.GetTruncatedDescription
151 getCategoryLabel: VideoMethods.GetCategoryLabel
152 getLicenceLabel: VideoMethods.GetLicenceLabel
153 getLanguageLabel: VideoMethods.GetLanguageLabel
145 154
146 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> 155 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
147 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string> 156 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
@@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
149} 158}
150 159
151export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} 160export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
161
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 02dde1726..94af1ece5 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -5,7 +5,6 @@ import { map, maxBy, truncate } from 'lodash'
5import * as parseTorrent from 'parse-torrent' 5import * as parseTorrent from 'parse-torrent'
6import { join } from 'path' 6import { join } from 'path'
7import * as Sequelize from 'sequelize' 7import * as Sequelize from 'sequelize'
8import * as Promise from 'bluebird'
9 8
10import { TagInstance } from './tag-interface' 9import { TagInstance } from './tag-interface'
11import { 10import {
@@ -52,6 +51,7 @@ import {
52 51
53 VideoMethods 52 VideoMethods
54} from './video-interface' 53} from './video-interface'
54import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
55 55
56let Video: Sequelize.Model<VideoInstance, VideoAttributes> 56let Video: Sequelize.Model<VideoInstance, VideoAttributes>
57let getOriginalFile: VideoMethods.GetOriginalFile 57let getOriginalFile: VideoMethods.GetOriginalFile
@@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName
64let isOwned: VideoMethods.IsOwned 64let isOwned: VideoMethods.IsOwned
65let toFormattedJSON: VideoMethods.ToFormattedJSON 65let toFormattedJSON: VideoMethods.ToFormattedJSON
66let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON 66let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
67let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON 67let toActivityPubObject: VideoMethods.ToActivityPubObject
68let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
69let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 68let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
70let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 69let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
71let createPreview: VideoMethods.CreatePreview 70let createPreview: VideoMethods.CreatePreview
@@ -76,21 +75,25 @@ let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
76let getEmbedPath: VideoMethods.GetEmbedPath 75let getEmbedPath: VideoMethods.GetEmbedPath
77let getDescriptionPath: VideoMethods.GetDescriptionPath 76let getDescriptionPath: VideoMethods.GetDescriptionPath
78let getTruncatedDescription: VideoMethods.GetTruncatedDescription 77let getTruncatedDescription: VideoMethods.GetTruncatedDescription
78let getCategoryLabel: VideoMethods.GetCategoryLabel
79let getLicenceLabel: VideoMethods.GetLicenceLabel
80let getLanguageLabel: VideoMethods.GetLanguageLabel
79 81
80let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 82let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
81let list: VideoMethods.List 83let list: VideoMethods.List
82let listForApi: VideoMethods.ListForApi 84let listForApi: VideoMethods.ListForApi
83let listUserVideosForApi: VideoMethods.ListUserVideosForApi 85let listUserVideosForApi: VideoMethods.ListUserVideosForApi
84let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 86let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
85let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags 87let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
86let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor 88let listOwnedByAccount: VideoMethods.ListOwnedByAccount
87let load: VideoMethods.Load 89let load: VideoMethods.Load
88let loadByUUID: VideoMethods.LoadByUUID 90let loadByUUID: VideoMethods.LoadByUUID
91let loadByUrl: VideoMethods.LoadByUrl
89let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 92let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
90let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor 93let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
91let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags 94let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
92let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags 95let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
93let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags 96let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
94let removeThumbnail: VideoMethods.RemoveThumbnail 97let removeThumbnail: VideoMethods.RemoveThumbnail
95let removePreview: VideoMethods.RemovePreview 98let removePreview: VideoMethods.RemovePreview
96let removeFile: VideoMethods.RemoveFile 99let removeFile: VideoMethods.RemoveFile
@@ -219,6 +222,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
219 type: DataTypes.BOOLEAN, 222 type: DataTypes.BOOLEAN,
220 allowNull: false, 223 allowNull: false,
221 defaultValue: false 224 defaultValue: false
225 },
226 url: {
227 type: DataTypes.STRING,
228 allowNull: false,
229 validate: {
230 isUrl: true
231 }
222 } 232 }
223 }, 233 },
224 { 234 {
@@ -243,6 +253,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
243 }, 253 },
244 { 254 {
245 fields: [ 'channelId' ] 255 fields: [ 'channelId' ]
256 },
257 {
258 fields: [ 'parentId' ]
246 } 259 }
247 ], 260 ],
248 hooks: { 261 hooks: {
@@ -258,16 +271,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
258 list, 271 list,
259 listForApi, 272 listForApi,
260 listUserVideosForApi, 273 listUserVideosForApi,
261 listOwnedAndPopulateAuthorAndTags, 274 listOwnedAndPopulateAccountAndTags,
262 listOwnedByAuthor, 275 listOwnedByAccount,
263 load, 276 load,
264 loadAndPopulateAuthor, 277 loadAndPopulateAccount,
265 loadAndPopulateAuthorAndPodAndTags, 278 loadAndPopulateAccountAndPodAndTags,
266 loadByHostAndUUID, 279 loadByHostAndUUID,
267 loadByUUID, 280 loadByUUID,
268 loadLocalVideoByUUID, 281 loadLocalVideoByUUID,
269 loadByUUIDAndPopulateAuthorAndPodAndTags, 282 loadByUUIDAndPopulateAccountAndPodAndTags,
270 searchAndPopulateAuthorAndPodAndTags 283 searchAndPopulateAccountAndPodAndTags
271 ] 284 ]
272 const instanceMethods = [ 285 const instanceMethods = [
273 createPreview, 286 createPreview,
@@ -286,16 +299,18 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
286 removePreview, 299 removePreview,
287 removeThumbnail, 300 removeThumbnail,
288 removeTorrent, 301 removeTorrent,
289 toAddRemoteJSON, 302 toActivityPubObject,
290 toFormattedJSON, 303 toFormattedJSON,
291 toFormattedDetailsJSON, 304 toFormattedDetailsJSON,
292 toUpdateRemoteJSON,
293 optimizeOriginalVideofile, 305 optimizeOriginalVideofile,
294 transcodeOriginalVideofile, 306 transcodeOriginalVideofile,
295 getOriginalFileHeight, 307 getOriginalFileHeight,
296 getEmbedPath, 308 getEmbedPath,
297 getTruncatedDescription, 309 getTruncatedDescription,
298 getDescriptionPath 310 getDescriptionPath,
311 getCategoryLabel,
312 getLicenceLabel,
313 getLanguageLabel
299 ] 314 ]
300 addMethodsToModel(Video, classMethods, instanceMethods) 315 addMethodsToModel(Video, classMethods, instanceMethods)
301 316
@@ -313,6 +328,14 @@ function associate (models) {
313 onDelete: 'cascade' 328 onDelete: 'cascade'
314 }) 329 })
315 330
331 Video.belongsTo(models.VideoChannel, {
332 foreignKey: {
333 name: 'parentId',
334 allowNull: true
335 },
336 onDelete: 'cascade'
337 })
338
316 Video.belongsToMany(models.Tag, { 339 Video.belongsToMany(models.Tag, {
317 foreignKey: 'videoId', 340 foreignKey: 'videoId',
318 through: models.VideoTag, 341 through: models.VideoTag,
@@ -423,7 +446,7 @@ getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance)
423 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 446 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
424} 447}
425 448
426createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { 449createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
427 const options = { 450 const options = {
428 announceList: [ 451 announceList: [
429 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] 452 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
@@ -433,18 +456,15 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil
433 ] 456 ]
434 } 457 }
435 458
436 return createTorrentPromise(this.getVideoFilePath(videoFile), options) 459 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
437 .then(torrent => {
438 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
439 logger.info('Creating torrent %s.', filePath)
440 460
441 return writeFilePromise(filePath, torrent).then(() => torrent) 461 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
442 }) 462 logger.info('Creating torrent %s.', filePath)
443 .then(torrent => {
444 const parsedTorrent = parseTorrent(torrent)
445 463
446 videoFile.infoHash = parsedTorrent.infoHash 464 await writeFilePromise(filePath, torrent)
447 }) 465
466 const parsedTorrent = parseTorrent(torrent)
467 videoFile.infoHash = parsedTorrent.infoHash
448} 468}
449 469
450getEmbedPath = function (this: VideoInstance) { 470getEmbedPath = function (this: VideoInstance) {
@@ -462,40 +482,28 @@ getPreviewPath = function (this: VideoInstance) {
462toFormattedJSON = function (this: VideoInstance) { 482toFormattedJSON = function (this: VideoInstance) {
463 let podHost 483 let podHost
464 484
465 if (this.VideoChannel.Author.Pod) { 485 if (this.VideoChannel.Account.Pod) {
466 podHost = this.VideoChannel.Author.Pod.host 486 podHost = this.VideoChannel.Account.Pod.host
467 } else { 487 } else {
468 // It means it's our video 488 // It means it's our video
469 podHost = CONFIG.WEBSERVER.HOST 489 podHost = CONFIG.WEBSERVER.HOST
470 } 490 }
471 491
472 // Maybe our pod is not up to date and there are new categories since our version
473 let categoryLabel = VIDEO_CATEGORIES[this.category]
474 if (!categoryLabel) categoryLabel = 'Misc'
475
476 // Maybe our pod is not up to date and there are new licences since our version
477 let licenceLabel = VIDEO_LICENCES[this.licence]
478 if (!licenceLabel) licenceLabel = 'Unknown'
479
480 // Language is an optional attribute
481 let languageLabel = VIDEO_LANGUAGES[this.language]
482 if (!languageLabel) languageLabel = 'Unknown'
483
484 const json = { 492 const json = {
485 id: this.id, 493 id: this.id,
486 uuid: this.uuid, 494 uuid: this.uuid,
487 name: this.name, 495 name: this.name,
488 category: this.category, 496 category: this.category,
489 categoryLabel, 497 categoryLabel: this.getCategoryLabel(),
490 licence: this.licence, 498 licence: this.licence,
491 licenceLabel, 499 licenceLabel: this.getLicenceLabel(),
492 language: this.language, 500 language: this.language,
493 languageLabel, 501 languageLabel: this.getLanguageLabel(),
494 nsfw: this.nsfw, 502 nsfw: this.nsfw,
495 description: this.getTruncatedDescription(), 503 description: this.getTruncatedDescription(),
496 podHost, 504 podHost,
497 isLocal: this.isOwned(), 505 isLocal: this.isOwned(),
498 author: this.VideoChannel.Author.name, 506 account: this.VideoChannel.Account.name,
499 duration: this.duration, 507 duration: this.duration,
500 views: this.views, 508 views: this.views,
501 likes: this.likes, 509 likes: this.likes,
@@ -552,75 +560,75 @@ toFormattedDetailsJSON = function (this: VideoInstance) {
552 return Object.assign(formattedJson, detailsJson) 560 return Object.assign(formattedJson, detailsJson)
553} 561}
554 562
555toAddRemoteJSON = function (this: VideoInstance) { 563toActivityPubObject = function (this: VideoInstance) {
556 // Get thumbnail data to send to the other pod 564 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
557 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
558 565
559 return readFileBufferPromise(thumbnailPath).then(thumbnailData => { 566 const tag = this.Tags.map(t => ({
560 const remoteVideo = { 567 type: 'Hashtag',
561 uuid: this.uuid, 568 name: t.name
562 name: this.name, 569 }))
563 category: this.category, 570
564 licence: this.licence, 571 const url = []
565 language: this.language, 572 for (const file of this.VideoFiles) {
566 nsfw: this.nsfw, 573 url.push({
567 truncatedDescription: this.getTruncatedDescription(), 574 type: 'Link',
568 channelUUID: this.VideoChannel.uuid, 575 mimeType: 'video/' + file.extname,
569 duration: this.duration, 576 url: getVideoFileUrl(this, file, baseUrlHttp),
570 thumbnailData: thumbnailData.toString('binary'), 577 width: file.resolution,
571 tags: map<TagInstance, string>(this.Tags, 'name'), 578 size: file.size
572 createdAt: this.createdAt, 579 })
573 updatedAt: this.updatedAt,
574 views: this.views,
575 likes: this.likes,
576 dislikes: this.dislikes,
577 privacy: this.privacy,
578 files: []
579 }
580 580
581 this.VideoFiles.forEach(videoFile => { 581 url.push({
582 remoteVideo.files.push({ 582 type: 'Link',
583 infoHash: videoFile.infoHash, 583 mimeType: 'application/x-bittorrent',
584 resolution: videoFile.resolution, 584 url: getTorrentUrl(this, file, baseUrlHttp),
585 extname: videoFile.extname, 585 width: file.resolution
586 size: videoFile.size
587 })
588 }) 586 })
589 587
590 return remoteVideo 588 url.push({
591 }) 589 type: 'Link',
592} 590 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
591 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
592 width: file.resolution
593 })
594 }
593 595
594toUpdateRemoteJSON = function (this: VideoInstance) { 596 const videoObject: VideoTorrentObject = {
595 const json = { 597 type: 'Video',
596 uuid: this.uuid,
597 name: this.name, 598 name: this.name,
598 category: this.category, 599 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
599 licence: this.licence, 600 duration: 'PT' + this.duration + 'S',
600 language: this.language, 601 uuid: this.uuid,
601 nsfw: this.nsfw, 602 tag,
602 truncatedDescription: this.getTruncatedDescription(), 603 category: {
603 duration: this.duration, 604 id: this.category,
604 tags: map<TagInstance, string>(this.Tags, 'name'), 605 label: this.getCategoryLabel()
605 createdAt: this.createdAt, 606 },
606 updatedAt: this.updatedAt, 607 licence: {
608 id: this.licence,
609 name: this.getLicenceLabel()
610 },
611 language: {
612 id: this.language,
613 name: this.getLanguageLabel()
614 },
607 views: this.views, 615 views: this.views,
608 likes: this.likes, 616 nsfw: this.nsfw,
609 dislikes: this.dislikes, 617 published: this.createdAt,
610 privacy: this.privacy, 618 updated: this.updatedAt,
611 files: [] 619 mediaType: 'text/markdown',
620 content: this.getTruncatedDescription(),
621 icon: {
622 type: 'Image',
623 url: getThumbnailUrl(this, baseUrlHttp),
624 mediaType: 'image/jpeg',
625 width: THUMBNAILS_SIZE.width,
626 height: THUMBNAILS_SIZE.height
627 },
628 url
612 } 629 }
613 630
614 this.VideoFiles.forEach(videoFile => { 631 return videoObject
615 json.files.push({
616 infoHash: videoFile.infoHash,
617 resolution: videoFile.resolution,
618 extname: videoFile.extname,
619 size: videoFile.size
620 })
621 })
622
623 return json
624} 632}
625 633
626getTruncatedDescription = function (this: VideoInstance) { 634getTruncatedDescription = function (this: VideoInstance) {
@@ -631,7 +639,7 @@ getTruncatedDescription = function (this: VideoInstance) {
631 return truncate(this.description, options) 639 return truncate(this.description, options)
632} 640}
633 641
634optimizeOriginalVideofile = function (this: VideoInstance) { 642optimizeOriginalVideofile = async function (this: VideoInstance) {
635 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 643 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
636 const newExtname = '.mp4' 644 const newExtname = '.mp4'
637 const inputVideoFile = this.getOriginalFile() 645 const inputVideoFile = this.getOriginalFile()
@@ -643,40 +651,32 @@ optimizeOriginalVideofile = function (this: VideoInstance) {
643 outputPath: videoOutputPath 651 outputPath: videoOutputPath
644 } 652 }
645 653
646 return transcode(transcodeOptions) 654 try {
647 .then(() => { 655 // Could be very long!
648 return unlinkPromise(videoInputPath) 656 await transcode(transcodeOptions)
649 })
650 .then(() => {
651 // Important to do this before getVideoFilename() to take in account the new file extension
652 inputVideoFile.set('extname', newExtname)
653 657
654 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) 658 await unlinkPromise(videoInputPath)
655 })
656 .then(() => {
657 return statPromise(this.getVideoFilePath(inputVideoFile))
658 })
659 .then(stats => {
660 return inputVideoFile.set('size', stats.size)
661 })
662 .then(() => {
663 return this.createTorrentAndSetInfoHash(inputVideoFile)
664 })
665 .then(() => {
666 return inputVideoFile.save()
667 })
668 .then(() => {
669 return undefined
670 })
671 .catch(err => {
672 // Auto destruction...
673 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
674 659
675 throw err 660 // Important to do this before getVideoFilename() to take in account the new file extension
676 }) 661 inputVideoFile.set('extname', newExtname)
662
663 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
664 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
665
666 inputVideoFile.set('size', stats.size)
667
668 await this.createTorrentAndSetInfoHash(inputVideoFile)
669 await inputVideoFile.save()
670
671 } catch (err) {
672 // Auto destruction...
673 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
674
675 throw err
676 }
677} 677}
678 678
679transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { 679transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
680 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 680 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
681 const extname = '.mp4' 681 const extname = '.mp4'
682 682
@@ -696,25 +696,18 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes
696 outputPath: videoOutputPath, 696 outputPath: videoOutputPath,
697 resolution 697 resolution
698 } 698 }
699 return transcode(transcodeOptions)
700 .then(() => {
701 return statPromise(videoOutputPath)
702 })
703 .then(stats => {
704 newVideoFile.set('size', stats.size)
705 699
706 return undefined 700 await transcode(transcodeOptions)
707 }) 701
708 .then(() => { 702 const stats = await statPromise(videoOutputPath)
709 return this.createTorrentAndSetInfoHash(newVideoFile) 703
710 }) 704 newVideoFile.set('size', stats.size)
711 .then(() => { 705
712 return newVideoFile.save() 706 await this.createTorrentAndSetInfoHash(newVideoFile)
713 }) 707
714 .then(() => { 708 await newVideoFile.save()
715 return this.VideoFiles.push(newVideoFile) 709
716 }) 710 this.VideoFiles.push(newVideoFile)
717 .then(() => undefined)
718} 711}
719 712
720getOriginalFileHeight = function (this: VideoInstance) { 713getOriginalFileHeight = function (this: VideoInstance) {
@@ -727,6 +720,31 @@ getDescriptionPath = function (this: VideoInstance) {
727 return `/api/${API_VERSION}/videos/${this.uuid}/description` 720 return `/api/${API_VERSION}/videos/${this.uuid}/description`
728} 721}
729 722
723getCategoryLabel = function (this: VideoInstance) {
724 let categoryLabel = VIDEO_CATEGORIES[this.category]
725
726 // Maybe our pod is not up to date and there are new categories since our version
727 if (!categoryLabel) categoryLabel = 'Misc'
728
729 return categoryLabel
730}
731
732getLicenceLabel = function (this: VideoInstance) {
733 let licenceLabel = VIDEO_LICENCES[this.licence]
734 // Maybe our pod is not up to date and there are new licences since our version
735 if (!licenceLabel) licenceLabel = 'Unknown'
736
737 return licenceLabel
738}
739
740getLanguageLabel = function (this: VideoInstance) {
741 // Language is an optional attribute
742 let languageLabel = VIDEO_LANGUAGES[this.language]
743 if (!languageLabel) languageLabel = 'Unknown'
744
745 return languageLabel
746}
747
730removeThumbnail = function (this: VideoInstance) { 748removeThumbnail = function (this: VideoInstance) {
731 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) 749 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
732 return unlinkPromise(thumbnailPath) 750 return unlinkPromise(thumbnailPath)
@@ -779,7 +797,7 @@ listUserVideosForApi = function (userId: number, start: number, count: number, s
779 required: true, 797 required: true,
780 include: [ 798 include: [
781 { 799 {
782 model: Video['sequelize'].models.Author, 800 model: Video['sequelize'].models.Account,
783 where: { 801 where: {
784 userId 802 userId
785 }, 803 },
@@ -810,7 +828,7 @@ listForApi = function (start: number, count: number, sort: string) {
810 model: Video['sequelize'].models.VideoChannel, 828 model: Video['sequelize'].models.VideoChannel,
811 include: [ 829 include: [
812 { 830 {
813 model: Video['sequelize'].models.Author, 831 model: Video['sequelize'].models.Account,
814 include: [ 832 include: [
815 { 833 {
816 model: Video['sequelize'].models.Pod, 834 model: Video['sequelize'].models.Pod,
@@ -846,7 +864,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
846 model: Video['sequelize'].models.VideoChannel, 864 model: Video['sequelize'].models.VideoChannel,
847 include: [ 865 include: [
848 { 866 {
849 model: Video['sequelize'].models.Author, 867 model: Video['sequelize'].models.Account,
850 include: [ 868 include: [
851 { 869 {
852 model: Video['sequelize'].models.Pod, 870 model: Video['sequelize'].models.Pod,
@@ -867,7 +885,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran
867 return Video.findOne(query) 885 return Video.findOne(query)
868} 886}
869 887
870listOwnedAndPopulateAuthorAndTags = function () { 888listOwnedAndPopulateAccountAndTags = function () {
871 const query = { 889 const query = {
872 where: { 890 where: {
873 remote: false 891 remote: false
@@ -876,7 +894,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
876 Video['sequelize'].models.VideoFile, 894 Video['sequelize'].models.VideoFile,
877 { 895 {
878 model: Video['sequelize'].models.VideoChannel, 896 model: Video['sequelize'].models.VideoChannel,
879 include: [ Video['sequelize'].models.Author ] 897 include: [ Video['sequelize'].models.Account ]
880 }, 898 },
881 Video['sequelize'].models.Tag 899 Video['sequelize'].models.Tag
882 ] 900 ]
@@ -885,7 +903,7 @@ listOwnedAndPopulateAuthorAndTags = function () {
885 return Video.findAll(query) 903 return Video.findAll(query)
886} 904}
887 905
888listOwnedByAuthor = function (author: string) { 906listOwnedByAccount = function (account: string) {
889 const query = { 907 const query = {
890 where: { 908 where: {
891 remote: false 909 remote: false
@@ -898,9 +916,9 @@ listOwnedByAuthor = function (author: string) {
898 model: Video['sequelize'].models.VideoChannel, 916 model: Video['sequelize'].models.VideoChannel,
899 include: [ 917 include: [
900 { 918 {
901 model: Video['sequelize'].models.Author, 919 model: Video['sequelize'].models.Account,
902 where: { 920 where: {
903 name: author 921 name: account
904 } 922 }
905 } 923 }
906 ] 924 ]
@@ -942,13 +960,13 @@ loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
942 return Video.findOne(query) 960 return Video.findOne(query)
943} 961}
944 962
945loadAndPopulateAuthor = function (id: number) { 963loadAndPopulateAccount = function (id: number) {
946 const options = { 964 const options = {
947 include: [ 965 include: [
948 Video['sequelize'].models.VideoFile, 966 Video['sequelize'].models.VideoFile,
949 { 967 {
950 model: Video['sequelize'].models.VideoChannel, 968 model: Video['sequelize'].models.VideoChannel,
951 include: [ Video['sequelize'].models.Author ] 969 include: [ Video['sequelize'].models.Account ]
952 } 970 }
953 ] 971 ]
954 } 972 }
@@ -956,14 +974,14 @@ loadAndPopulateAuthor = function (id: number) {
956 return Video.findById(id, options) 974 return Video.findById(id, options)
957} 975}
958 976
959loadAndPopulateAuthorAndPodAndTags = function (id: number) { 977loadAndPopulateAccountAndPodAndTags = function (id: number) {
960 const options = { 978 const options = {
961 include: [ 979 include: [
962 { 980 {
963 model: Video['sequelize'].models.VideoChannel, 981 model: Video['sequelize'].models.VideoChannel,
964 include: [ 982 include: [
965 { 983 {
966 model: Video['sequelize'].models.Author, 984 model: Video['sequelize'].models.Account,
967 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 985 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
968 } 986 }
969 ] 987 ]
@@ -976,7 +994,7 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) {
976 return Video.findById(id, options) 994 return Video.findById(id, options)
977} 995}
978 996
979loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { 997loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
980 const options = { 998 const options = {
981 where: { 999 where: {
982 uuid 1000 uuid
@@ -986,7 +1004,7 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
986 model: Video['sequelize'].models.VideoChannel, 1004 model: Video['sequelize'].models.VideoChannel,
987 include: [ 1005 include: [
988 { 1006 {
989 model: Video['sequelize'].models.Author, 1007 model: Video['sequelize'].models.Account,
990 include: [ { model: Video['sequelize'].models.Pod, required: false } ] 1008 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
991 } 1009 }
992 ] 1010 ]
@@ -999,20 +1017,20 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
999 return Video.findOne(options) 1017 return Video.findOne(options)
1000} 1018}
1001 1019
1002searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { 1020searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1003 const podInclude: Sequelize.IncludeOptions = { 1021 const podInclude: Sequelize.IncludeOptions = {
1004 model: Video['sequelize'].models.Pod, 1022 model: Video['sequelize'].models.Pod,
1005 required: false 1023 required: false
1006 } 1024 }
1007 1025
1008 const authorInclude: Sequelize.IncludeOptions = { 1026 const accountInclude: Sequelize.IncludeOptions = {
1009 model: Video['sequelize'].models.Author, 1027 model: Video['sequelize'].models.Account,
1010 include: [ podInclude ] 1028 include: [ podInclude ]
1011 } 1029 }
1012 1030
1013 const videoChannelInclude: Sequelize.IncludeOptions = { 1031 const videoChannelInclude: Sequelize.IncludeOptions = {
1014 model: Video['sequelize'].models.VideoChannel, 1032 model: Video['sequelize'].models.VideoChannel,
1015 include: [ authorInclude ], 1033 include: [ accountInclude ],
1016 required: true 1034 required: true
1017 } 1035 }
1018 1036
@@ -1045,8 +1063,8 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s
1045 } 1063 }
1046 } 1064 }
1047 podInclude.required = true 1065 podInclude.required = true
1048 } else if (field === 'author') { 1066 } else if (field === 'account') {
1049 authorInclude.where = { 1067 accountInclude.where = {
1050 name: { 1068 name: {
1051 [Sequelize.Op.iLike]: '%' + value + '%' 1069 [Sequelize.Op.iLike]: '%' + value + '%'
1052 } 1070 }
@@ -1090,13 +1108,17 @@ function getBaseUrls (video: VideoInstance) {
1090 baseUrlHttp = CONFIG.WEBSERVER.URL 1108 baseUrlHttp = CONFIG.WEBSERVER.URL
1091 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 1109 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1092 } else { 1110 } else {
1093 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host 1111 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
1094 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host 1112 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
1095 } 1113 }
1096 1114
1097 return { baseUrlHttp, baseUrlWs } 1115 return { baseUrlHttp, baseUrlWs }
1098} 1116}
1099 1117
1118function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1119 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1120}
1121
1100function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { 1122function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1101 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) 1123 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1102} 1124}