diff options
Diffstat (limited to 'server')
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 | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { database as db } from '../../initializers' | ||
5 | import { executeIfActivityPub, localAccountValidator } from '../../middlewares' | ||
6 | import { pageToStartAndCount } from '../../helpers' | ||
7 | import { AccountInstance } from '../../models' | ||
8 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | ||
9 | import { ACTIVITY_PUB } from '../../initializers/constants' | ||
10 | import { asyncMiddleware } from '../../middlewares/async' | ||
11 | |||
12 | const activityPubClientRouter = express.Router() | ||
13 | |||
14 | activityPubClientRouter.get('/account/:name', | ||
15 | executeIfActivityPub(localAccountValidator), | ||
16 | executeIfActivityPub(asyncMiddleware(accountController)) | ||
17 | ) | ||
18 | |||
19 | activityPubClientRouter.get('/account/:name/followers', | ||
20 | executeIfActivityPub(localAccountValidator), | ||
21 | executeIfActivityPub(asyncMiddleware(accountFollowersController)) | ||
22 | ) | ||
23 | |||
24 | activityPubClientRouter.get('/account/:name/following', | ||
25 | executeIfActivityPub(localAccountValidator), | ||
26 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) | ||
27 | ) | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | activityPubClientRouter | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | async 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 | |||
43 | async 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 | |||
55 | async 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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { | ||
4 | processCreateActivity, | ||
5 | processUpdateActivity, | ||
6 | processFlagActivity | ||
7 | } from '../../lib' | ||
8 | import { | ||
9 | Activity, | ||
10 | ActivityType, | ||
11 | RootActivity, | ||
12 | ActivityPubCollection, | ||
13 | ActivityPubOrderedCollection | ||
14 | } from '../../../shared' | ||
15 | import { | ||
16 | signatureValidator, | ||
17 | checkSignature, | ||
18 | asyncMiddleware | ||
19 | } from '../../middlewares' | ||
20 | import { logger } from '../../helpers' | ||
21 | |||
22 | const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { | ||
23 | Create: processCreateActivity, | ||
24 | Update: processUpdateActivity, | ||
25 | Flag: processFlagActivity | ||
26 | } | ||
27 | |||
28 | const inboxRouter = express.Router() | ||
29 | |||
30 | inboxRouter.post('/', | ||
31 | signatureValidator, | ||
32 | asyncMiddleware(checkSignature), | ||
33 | // inboxValidator, | ||
34 | asyncMiddleware(inboxController) | ||
35 | ) | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
39 | export { | ||
40 | inboxRouter | ||
41 | } | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | async 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 | |||
62 | async 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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { badRequest } from '../../helpers' | ||
4 | import { inboxRouter } from './inbox' | ||
5 | |||
6 | const remoteRouter = express.Router() | ||
7 | |||
8 | remoteRouter.use('/inbox', inboxRouter) | ||
9 | remoteRouter.use('/*', badRequest) | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
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 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { badRequest } from '../../../helpers' | ||
4 | |||
5 | import { remotePodsRouter } from './pods' | ||
6 | import { remoteVideosRouter } from './videos' | ||
7 | |||
8 | const remoteRouter = express.Router() | ||
9 | |||
10 | remoteRouter.use('/pods', remotePodsRouter) | ||
11 | remoteRouter.use('/videos', remoteVideosRouter) | ||
12 | remoteRouter.use('/*', badRequest) | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
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 @@ | |||
1 | import * as url from 'url' | ||
2 | |||
3 | import { database as db } from '../initializers' | ||
4 | import { logger } from './logger' | ||
5 | import { doRequest } from './requests' | ||
6 | import { isRemoteAccountValid } from './custom-validators' | ||
7 | import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' | ||
8 | import { ResultList } from '../../shared/models/result-list.model' | ||
9 | |||
10 | async 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 | |||
59 | function 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 | |||
78 | function 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 | |||
100 | export { | ||
101 | fetchRemoteAccountAndCreatePod, | ||
102 | activityPubContextify, | ||
103 | activityPubCollectionPagination | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | async 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' | |||
19 | import * as bcrypt from 'bcrypt' | 19 | import * as bcrypt from 'bcrypt' |
20 | import * as createTorrent from 'create-torrent' | 20 | import * as createTorrent from 'create-torrent' |
21 | import * as rimraf from 'rimraf' | 21 | import * as rimraf from 'rimraf' |
22 | import * as openssl from 'openssl-wrapper' | 22 | import * as pem from 'pem' |
23 | import * as Promise from 'bluebird' | 23 | import * as jsonld from 'jsonld' |
24 | import * as jsig from 'jsonld-signatures' | ||
25 | jsig.use('jsonld', jsonld) | ||
24 | 26 | ||
25 | function isTestInstance () { | 27 | function 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 | ||
59 | function pageToStartAndCount (page: number, itemsPerPage: number) { | ||
60 | const start = (page - 1) * itemsPerPage | ||
61 | |||
62 | return { start, count: itemsPerPage } | ||
63 | } | ||
64 | |||
57 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 65 | function 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) | |||
104 | const mkdirpPromise = promisify1<string, string>(mkdirp) | 112 | const mkdirpPromise = promisify1<string, string>(mkdirp) |
105 | const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes) | 113 | const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes) |
106 | const accessPromise = promisify1WithVoid<string | Buffer>(access) | 114 | const accessPromise = promisify1WithVoid<string | Buffer>(access) |
107 | const opensslExecPromise = promisify2WithVoid<string, any>(openssl.exec) | 115 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) |
116 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | ||
108 | const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) | 117 | const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) |
109 | const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) | 118 | const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) |
110 | const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash) | 119 | const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash) |
111 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) | 120 | const createTorrentPromise = promisify2<string, any, any>(createTorrent) |
112 | const rimrafPromise = promisify1WithVoid<string>(rimraf) | 121 | const rimrafPromise = promisify1WithVoid<string>(rimraf) |
113 | const statPromise = promisify1<string, Stats>(stat) | 122 | const statPromise = promisify1<string, Stats>(stat) |
123 | const jsonldSignPromise = promisify2<object, { privateKeyPem: string, creator: string }, object>(jsig.sign) | ||
124 | const 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 @@ | |||
1 | import * as validator from 'validator' | ||
2 | |||
3 | import { exists, isUUIDValid } from '../misc' | ||
4 | import { isActivityPubUrlValid } from './misc' | ||
5 | import { isUserUsernameValid } from '../users' | ||
6 | |||
7 | function isAccountEndpointsObjectValid (endpointObject: any) { | ||
8 | return isAccountSharedInboxValid(endpointObject.sharedInbox) | ||
9 | } | ||
10 | |||
11 | function isAccountSharedInboxValid (sharedInbox: string) { | ||
12 | return isActivityPubUrlValid(sharedInbox) | ||
13 | } | ||
14 | |||
15 | function isAccountPublicKeyObjectValid (publicKeyObject: any) { | ||
16 | return isAccountPublicKeyIdValid(publicKeyObject.id) && | ||
17 | isAccountPublicKeyOwnerValid(publicKeyObject.owner) && | ||
18 | isAccountPublicKeyValid(publicKeyObject.publicKeyPem) | ||
19 | } | ||
20 | |||
21 | function isAccountPublicKeyIdValid (id: string) { | ||
22 | return isActivityPubUrlValid(id) | ||
23 | } | ||
24 | |||
25 | function isAccountTypeValid (type: string) { | ||
26 | return type === 'Person' || type === 'Application' | ||
27 | } | ||
28 | |||
29 | function isAccountPublicKeyOwnerValid (owner: string) { | ||
30 | return isActivityPubUrlValid(owner) | ||
31 | } | ||
32 | |||
33 | function 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 | |||
40 | function isAccountIdValid (id: string) { | ||
41 | return isActivityPubUrlValid(id) | ||
42 | } | ||
43 | |||
44 | function isAccountFollowingValid (id: string) { | ||
45 | return isActivityPubUrlValid(id) | ||
46 | } | ||
47 | |||
48 | function isAccountFollowersValid (id: string) { | ||
49 | return isActivityPubUrlValid(id) | ||
50 | } | ||
51 | |||
52 | function isAccountInboxValid (inbox: string) { | ||
53 | return isActivityPubUrlValid(inbox) | ||
54 | } | ||
55 | |||
56 | function isAccountOutboxValid (outbox: string) { | ||
57 | return isActivityPubUrlValid(outbox) | ||
58 | } | ||
59 | |||
60 | function isAccountNameValid (name: string) { | ||
61 | return isUserUsernameValid(name) | ||
62 | } | ||
63 | |||
64 | function isAccountPreferredUsernameValid (preferredUsername: string) { | ||
65 | return isAccountNameValid(preferredUsername) | ||
66 | } | ||
67 | |||
68 | function isAccountUrlValid (url: string) { | ||
69 | return isActivityPubUrlValid(url) | ||
70 | } | ||
71 | |||
72 | function 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 | |||
79 | function 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 | |||
93 | function isAccountFollowingCountValid (value: string) { | ||
94 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
95 | } | ||
96 | |||
97 | function isAccountFollowersCountValid (value: string) { | ||
98 | return exists(value) && validator.isInt('' + value, { min: 0 }) | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | export { | ||
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 @@ | |||
1 | export * from './account' | ||
2 | export * from './signature' | ||
3 | export * from './misc' | ||
4 | export * from './videos' | ||
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts new file mode 100644 index 000000000..806d33483 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import { exists } from '../misc' | ||
2 | |||
3 | function 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 | |||
15 | export { | ||
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 @@ | |||
1 | import { exists } from '../misc' | ||
2 | import { isActivityPubUrlValid } from './misc' | ||
3 | |||
4 | function isSignatureTypeValid (signatureType: string) { | ||
5 | return exists(signatureType) && signatureType === 'GraphSignature2012' | ||
6 | } | ||
7 | |||
8 | function isSignatureCreatorValid (signatureCreator: string) { | ||
9 | return exists(signatureCreator) && isActivityPubUrlValid(signatureCreator) | ||
10 | } | ||
11 | |||
12 | function isSignatureValueValid (signatureValue: string) { | ||
13 | return exists(signatureValue) && signatureValue.length > 0 | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
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 @@ | |||
1 | export * from './remote' | 1 | export * from './activitypub' |
2 | export * from './misc' | 2 | export * from './misc' |
3 | export * from './pods' | 3 | export * from './pods' |
4 | export * from './pods' | 4 | export * from './pods' |
diff --git a/server/helpers/custom-validators/remote/index.ts b/server/helpers/custom-validators/remote/index.ts deleted file mode 100644 index e29a9b767..000000000 --- a/server/helpers/custom-validators/remote/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './videos' | ||
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f18b6bd9a..c07dddefe 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
3 | 2 | ||
4 | import { CONFIG } from '../initializers' | 3 | import { CONFIG } from '../initializers' |
diff --git a/server/helpers/index.ts b/server/helpers/index.ts index 846bd796f..2c7ac3954 100644 --- a/server/helpers/index.ts +++ b/server/helpers/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './activitypub' | ||
1 | export * from './core-utils' | 2 | export * from './core-utils' |
2 | export * from './logger' | 3 | export * from './logger' |
3 | export * from './custom-validators' | 4 | export * from './custom-validators' |
@@ -6,3 +7,4 @@ export * from './database-utils' | |||
6 | export * from './peertube-crypto' | 7 | export * from './peertube-crypto' |
7 | export * from './requests' | 8 | export * from './requests' |
8 | export * from './utils' | 9 | export * from './utils' |
10 | export * from './webfinger' | ||
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 10a226af4..6d50e446f 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -1,77 +1,68 @@ | |||
1 | import * as crypto from 'crypto' | 1 | import * as jsig from 'jsonld-signatures' |
2 | import { join } from 'path' | ||
3 | 2 | ||
4 | import { | 3 | import { |
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' |
12 | import { | 7 | import { |
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' |
20 | import { logger } from './logger' | 16 | import { logger } from './logger' |
17 | import { AccountInstance } from '../models/account/account-interface' | ||
21 | 18 | ||
22 | function checkSignature (publicKey: string, data: string, hexSignature: string) { | 19 | async 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 | ||
43 | async function sign (data: string | Object) { | 28 | function 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 | ||
64 | function 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 | ||
68 | async function createCertsIfNotExist () { | 55 | function 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 | |||
64 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
65 | return bcryptComparePromise(plainPassword, hashPassword) | ||
75 | } | 66 | } |
76 | 67 | ||
77 | async function cryptPassword (password: string) { | 68 | async 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 | ||
83 | function getMyPrivateCert () { | ||
84 | const certPath = join(CONFIG.STORAGE.CERT_DIR, PRIVATE_CERT_NAME) | ||
85 | return readFilePromise(certPath, 'utf8') | ||
86 | } | ||
87 | |||
88 | function getMyPublicCert () { | ||
89 | const certPath = join(CONFIG.STORAGE.CERT_DIR, PUBLIC_CERT_NAME) | ||
90 | return readFilePromise(certPath, 'utf8') | ||
91 | } | ||
92 | |||
93 | // --------------------------------------------------------------------------- | 74 | // --------------------------------------------------------------------------- |
94 | 75 | ||
95 | export { | 76 | export { |
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 | |||
107 | async 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 | |||
120 | async 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' |
10 | import { PodInstance } from '../models' | 10 | import { PodInstance } from '../models' |
11 | import { PodSignature } from '../../shared' | 11 | import { PodSignature } from '../../shared' |
12 | import { sign } from './peertube-crypto' | 12 | import { signObject } from './peertube-crypto' |
13 | |||
14 | function 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 | ||
14 | type MakeRetryRequestParams = { | 20 | type MakeRetryRequestParams = { |
15 | url: string, | 21 | url: string, |
@@ -31,61 +37,57 @@ function makeRetryRequest (params: MakeRetryRequestParams) { | |||
31 | } | 37 | } |
32 | 38 | ||
33 | type MakeSecureRequestParams = { | 39 | type 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 | } |
39 | function makeSecureRequest (params: MakeSecureRequestParams) { | 44 | function 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 | ||
88 | export { | 89 | export { |
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 @@ | |||
1 | import * as WebFinger from 'webfinger.js' | ||
2 | |||
3 | import { isTestInstance } from './core-utils' | ||
4 | import { isActivityPubUrlValid } from './custom-validators' | ||
5 | import { WebFingerData } from '../../shared' | ||
6 | import { fetchRemoteAccountAndCreatePod } from './activitypub' | ||
7 | |||
8 | const webfinger = new WebFinger({ | ||
9 | webfist_fallback: false, | ||
10 | tls_only: isTestInstance(), | ||
11 | uri_fallback: false, | ||
12 | request_timeout: 3000 | ||
13 | }) | ||
14 | |||
15 | async 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 | |||
30 | export { | ||
31 | getAccountFromWebfinger | ||
32 | } | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | function 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 | ||
3 | import { promisify0 } from '../helpers/core-utils' | 3 | import { promisify0 } from '../helpers/core-utils' |
4 | import { OAuthClientModel } from '../models/oauth/oauth-client-interface' | 4 | import { OAuthClientModel } from '../models/oauth/oauth-client-interface' |
5 | import { UserModel } from '../models/user/user-interface' | 5 | import { UserModel } from '../models/account/user-interface' |
6 | 6 | ||
7 | // Some checks on configuration files | 7 | // Some checks on configuration files |
8 | function checkConfig () { | 8 | function checkConfig () { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index d349abaf0..cb838cf16 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -10,7 +10,8 @@ import { | |||
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' |
15 | import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum' | 16 | import { 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 | ||
214 | const 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 | } |
295 | const 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 |
292 | const JOBS_FETCH_LIMIT_PER_CYCLE = 10 | 300 | const JOBS_FETCH_LIMIT_PER_CYCLE = { |
301 | transcoding: 10, | ||
302 | httpRequest: 20 | ||
303 | } | ||
293 | // 1 minutes | 304 | // 1 minutes |
294 | let JOBS_FETCHING_INTERVAL = 60000 | 305 | let JOBS_FETCHING_INTERVAL = 60000 |
295 | 306 | ||
296 | // --------------------------------------------------------------------------- | 307 | // --------------------------------------------------------------------------- |
297 | 308 | ||
298 | const PRIVATE_CERT_NAME = 'peertube.key.pem' | 309 | // const SIGNATURE_ALGORITHM = 'RSA-SHA256' |
299 | const PUBLIC_CERT_NAME = 'peertube.pub' | 310 | // const SIGNATURE_ENCODING = 'hex' |
300 | const SIGNATURE_ALGORITHM = 'RSA-SHA256' | 311 | const PRIVATE_RSA_KEY_SIZE = 2048 |
301 | const SIGNATURE_ENCODING = 'hex' | ||
302 | 312 | ||
303 | // Password encryption | 313 | // Password encryption |
304 | const BCRYPT_SALT_SIZE = 10 | 314 | const 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 | |||
15 | import { VideoFileModel } from './../models/video/video-file-interface' | 15 | import { VideoFileModel } from './../models/video/video-file-interface' |
16 | import { VideoAbuseModel } from './../models/video/video-abuse-interface' | 16 | import { VideoAbuseModel } from './../models/video/video-abuse-interface' |
17 | import { VideoChannelModel } from './../models/video/video-channel-interface' | 17 | import { VideoChannelModel } from './../models/video/video-channel-interface' |
18 | import { UserModel } from './../models/user/user-interface' | 18 | import { UserModel } from '../models/account/user-interface' |
19 | import { UserVideoRateModel } from './../models/user/user-video-rate-interface' | 19 | import { AccountVideoRateModel } from '../models/account/account-video-rate-interface' |
20 | import { AccountFollowModel } from '../models/account/account-follow-interface' | ||
20 | import { TagModel } from './../models/video/tag-interface' | 21 | import { TagModel } from './../models/video/tag-interface' |
21 | import { RequestModel } from './../models/request/request-interface' | 22 | import { RequestModel } from './../models/request/request-interface' |
22 | import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface' | 23 | import { RequestVideoQaduModel } from './../models/request/request-video-qadu-interface' |
@@ -26,7 +27,7 @@ import { PodModel } from './../models/pod/pod-interface' | |||
26 | import { OAuthTokenModel } from './../models/oauth/oauth-token-interface' | 27 | import { OAuthTokenModel } from './../models/oauth/oauth-token-interface' |
27 | import { OAuthClientModel } from './../models/oauth/oauth-client-interface' | 28 | import { OAuthClientModel } from './../models/oauth/oauth-client-interface' |
28 | import { JobModel } from './../models/job/job-interface' | 29 | import { JobModel } from './../models/job/job-interface' |
29 | import { AuthorModel } from './../models/video/author-interface' | 30 | import { AccountModel } from './../models/account/account-interface' |
30 | import { ApplicationModel } from './../models/application/application-interface' | 31 | import { ApplicationModel } from './../models/application/application-interface' |
31 | 32 | ||
32 | const dbname = CONFIG.DATABASE.DBNAME | 33 | const 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 @@ | |||
1 | export * from './process-create' | ||
2 | export * from './process-flag' | ||
3 | export * from './process-update' | ||
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts new file mode 100644 index 000000000..114ff1848 --- /dev/null +++ b/server/lib/activitypub/process-create.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import { | ||
2 | ActivityCreate, | ||
3 | VideoTorrentObject, | ||
4 | VideoChannelObject | ||
5 | } from '../../../shared' | ||
6 | import { database as db } from '../../initializers' | ||
7 | import { logger, retryTransactionWrapper } from '../../helpers' | ||
8 | |||
9 | function 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 | |||
25 | export { | ||
26 | processCreateActivity | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | function 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 | |||
40 | async 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 | |||
102 | function 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 @@ | |||
1 | import { | ||
2 | ActivityCreate, | ||
3 | VideoTorrentObject, | ||
4 | VideoChannelObject | ||
5 | } from '../../../shared' | ||
6 | |||
7 | function processFlagActivity (activity: ActivityCreate) { | ||
8 | // empty | ||
9 | } | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
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 @@ | |||
1 | import { | ||
2 | ActivityCreate, | ||
3 | VideoTorrentObject, | ||
4 | VideoChannelObject | ||
5 | } from '../../../shared' | ||
6 | |||
7 | function 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 | |||
17 | export { | ||
18 | processUpdateActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | function processUpdateVideo (video: VideoTorrentObject) { | ||
24 | |||
25 | } | ||
26 | |||
27 | function 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { | ||
4 | AccountInstance, | ||
5 | VideoInstance, | ||
6 | VideoChannelInstance | ||
7 | } from '../../models' | ||
8 | import { httpRequestJobScheduler } from '../jobs' | ||
9 | import { signObject, activityPubContextify } from '../../helpers' | ||
10 | import { Activity } from '../../../shared' | ||
11 | |||
12 | function 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 | |||
19 | function 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 | |||
26 | function 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 | |||
33 | function 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 | |||
40 | function 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 | |||
47 | function 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 | |||
56 | export { | ||
57 | |||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | function broadcastToFollowers (data: any, t: Sequelize.Transaction) { | ||
63 | return httpRequestJobScheduler.createJob(t, 'http-request', 'httpRequestBroadcastHandler', data) | ||
64 | } | ||
65 | |||
66 | function buildSignedActivity (byAccount: AccountInstance, data: Object) { | ||
67 | const activity = activityPubContextify(data) | ||
68 | |||
69 | return signObject(byAccount, activity) as Promise<Activity> | ||
70 | } | ||
71 | |||
72 | async function getPublicActivityTo (account: AccountInstance) { | ||
73 | const inboxUrls = await account.getFollowerSharedInboxUrls() | ||
74 | |||
75 | return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public') | ||
76 | } | ||
77 | |||
78 | async 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 | |||
91 | async 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 | |||
104 | async 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 | |||
117 | async 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 @@ | |||
1 | export * from './activitypub' | ||
1 | export * from './cache' | 2 | export * from './cache' |
2 | export * from './jobs' | 3 | export * from './jobs' |
3 | export * from './request' | 4 | export * from './request' |
diff --git a/server/lib/jobs/handlers/index.ts b/server/lib/jobs/handlers/index.ts deleted file mode 100644 index cef1f89a9..000000000 --- a/server/lib/jobs/handlers/index.ts +++ /dev/null | |||
@@ -1,17 +0,0 @@ | |||
1 | import * as videoFileOptimizer from './video-file-optimizer' | ||
2 | import * as videoFileTranscoder from './video-file-transcoder' | ||
3 | |||
4 | export interface JobHandler<T> { | ||
5 | process (data: object, jobId: number): T | ||
6 | onError (err: Error, jobId: number) | ||
7 | onSuccess (jobId: number, jobResult: T) | ||
8 | } | ||
9 | |||
10 | const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = { | ||
11 | videoFileOptimizer, | ||
12 | videoFileTranscoder | ||
13 | } | ||
14 | |||
15 | export { | ||
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | |||
3 | import { database as db } from '../../../initializers/database' | ||
4 | import { logger } from '../../../helpers' | ||
5 | |||
6 | async function process (data: { videoUUID: string }, jobId: number) { | ||
7 | |||
8 | } | ||
9 | |||
10 | function onError (err: Error, jobId: number) { | ||
11 | logger.error('Error when optimized video file in job %d.', jobId, err) | ||
12 | return Promise.resolve() | ||
13 | } | ||
14 | |||
15 | async function onSuccess (jobId: number) { | ||
16 | |||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
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 @@ | |||
1 | import { JobScheduler, JobHandler } from '../job-scheduler' | ||
2 | |||
3 | import * as httpRequestBroadcastHandler from './http-request-broadcast-handler' | ||
4 | import * as httpRequestUnicastHandler from './http-request-unicast-handler' | ||
5 | import { JobCategory } from '../../../../shared' | ||
6 | |||
7 | const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = { | ||
8 | httpRequestBroadcastHandler, | ||
9 | httpRequestUnicastHandler | ||
10 | } | ||
11 | const jobCategory: JobCategory = 'http-request' | ||
12 | |||
13 | const httpRequestJobScheduler = new JobScheduler(jobCategory, jobHandlers) | ||
14 | |||
15 | export { | ||
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | |||
3 | import { database as db } from '../../../initializers/database' | ||
4 | import { logger } from '../../../helpers' | ||
5 | |||
6 | async function process (data: { videoUUID: string }, jobId: number) { | ||
7 | |||
8 | } | ||
9 | |||
10 | function onError (err: Error, jobId: number) { | ||
11 | logger.error('Error when optimized video file in job %d.', jobId, err) | ||
12 | return Promise.resolve() | ||
13 | } | ||
14 | |||
15 | async function onSuccess (jobId: number) { | ||
16 | |||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
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 @@ | |||
1 | export * from './job-scheduler' | 1 | export * from './http-request-job-scheduler' |
2 | export * from './transcoding-job-scheduler' | ||
diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts index 61d483268..89a4bca88 100644 --- a/server/lib/jobs/job-scheduler.ts +++ b/server/lib/jobs/job-scheduler.ts | |||
@@ -1,39 +1,41 @@ | |||
1 | import { AsyncQueue, forever, queue } from 'async' | 1 | import { AsyncQueue, forever, queue } from 'async' |
2 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | 3 | ||
4 | import { database as db } from '../../initializers/database' | ||
5 | import { | 4 | import { |
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' |
10 | import { logger } from '../../helpers' | 10 | import { logger } from '../../helpers' |
11 | import { JobInstance } from '../../models' | 11 | import { JobInstance } from '../../models' |
12 | import { JobHandler, jobHandlers } from './handlers' | 12 | import { JobCategory } from '../../../shared' |
13 | 13 | ||
14 | export interface JobHandler<T> { | ||
15 | process (data: object, jobId: number): T | ||
16 | onError (err: Error, jobId: number) | ||
17 | onSuccess (jobId: number, jobResult: T) | ||
18 | } | ||
14 | type JobQueueCallback = (err: Error) => void | 19 | type JobQueueCallback = (err: Error) => void |
15 | 20 | ||
16 | class JobScheduler { | 21 | class 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 @@ | |||
1 | import { JobScheduler, JobHandler } from '../job-scheduler' | ||
2 | |||
3 | import * as videoFileOptimizer from './video-file-optimizer-handler' | ||
4 | import * as videoFileTranscoder from './video-file-transcoder-handler' | ||
5 | import { JobCategory } from '../../../../shared' | ||
6 | |||
7 | const jobHandlers: { [ handlerName: string ]: JobHandler<any> } = { | ||
8 | videoFileOptimizer, | ||
9 | videoFileTranscoder | ||
10 | } | ||
11 | const jobCategory: JobCategory = 'transcoding' | ||
12 | |||
13 | const transcodingJobScheduler = new JobScheduler(jobCategory, jobHandlers) | ||
14 | |||
15 | export { | ||
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 @@ | |||
1 | import { database as db } from '../initializers' | 1 | import { database as db } from '../initializers' |
2 | import { UserInstance } from '../models' | 2 | import { UserInstance } from '../models' |
3 | import { addVideoAuthorToFriends } from './friends' | 3 | import { addVideoAccountToFriends } from './friends' |
4 | import { createVideoChannel } from './video-channel' | 4 | import { createVideoChannel } from './video-channel' |
5 | 5 | ||
6 | async function createUserAuthorAndChannel (user: UserInstance, validateUser = true) { | 6 | async 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 | ||
40 | export { | 40 | export { |
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' | |||
3 | import { addVideoChannelToFriends } from './friends' | 3 | import { addVideoChannelToFriends } from './friends' |
4 | import { database as db } from '../initializers' | 4 | import { database as db } from '../initializers' |
5 | import { logger } from '../helpers' | 5 | import { logger } from '../helpers' |
6 | import { AuthorInstance } from '../models' | 6 | import { AccountInstance } from '../models' |
7 | import { VideoChannelCreate } from '../../shared/models' | 7 | import { VideoChannelCreate } from '../../shared/models' |
8 | 8 | ||
9 | async function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) { | 9 | async 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 @@ | |||
1 | import { Request, Response, NextFunction } from 'express' | ||
2 | |||
3 | import { database as db } from '../initializers' | ||
4 | import { | ||
5 | logger, | ||
6 | getAccountFromWebfinger, | ||
7 | isSignatureVerified | ||
8 | } from '../helpers' | ||
9 | import { ActivityPubSignature } from '../../shared' | ||
10 | |||
11 | async 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 | |||
38 | function 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 | |||
54 | export { | ||
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 @@ | |||
1 | export * from './validators' | 1 | export * from './validators' |
2 | export * from './activitypub' | ||
2 | export * from './async' | 3 | export * from './async' |
3 | export * from './oauth' | 4 | export * from './oauth' |
4 | export * from './pagination' | 5 | export * from './pagination' |
5 | export * from './pods' | 6 | export * from './pods' |
6 | export * from './search' | 7 | export * from './search' |
7 | export * from './secure' | ||
8 | export * from './sort' | 8 | export * from './sort' |
9 | export * from './user-right' | 9 | export * from './user-right' |
diff --git a/server/middlewares/secure.ts b/server/middlewares/secure.ts deleted file mode 100644 index 5dd809f15..000000000 --- a/server/middlewares/secure.ts +++ /dev/null | |||
@@ -1,55 +0,0 @@ | |||
1 | import 'express-validator' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { database as db } from '../initializers' | ||
5 | import { | ||
6 | logger, | ||
7 | checkSignature as peertubeCryptoCheckSignature | ||
8 | } from '../helpers' | ||
9 | import { PodSignature } from '../../shared' | ||
10 | |||
11 | async 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 | |||
53 | export { | ||
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 @@ | |||
1 | import { param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { database as db } from '../../initializers/database' | ||
5 | import { checkErrors } from './utils' | ||
6 | import { | ||
7 | logger, | ||
8 | isUserUsernameValid, | ||
9 | isUserPasswordValid, | ||
10 | isUserVideoQuotaValid, | ||
11 | isUserDisplayNSFWValid, | ||
12 | isUserRoleValid, | ||
13 | isAccountNameValid | ||
14 | } from '../../helpers' | ||
15 | import { AccountInstance } from '../../models' | ||
16 | |||
17 | const 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 | |||
31 | export { | ||
32 | localAccountValidator | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function 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 @@ | |||
1 | import { body } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { | ||
5 | logger, | ||
6 | isDateValid, | ||
7 | isSignatureTypeValid, | ||
8 | isSignatureCreatorValid, | ||
9 | isSignatureValueValid | ||
10 | } from '../../../helpers' | ||
11 | import { checkErrors } from '../utils' | ||
12 | |||
13 | const 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 | |||
28 | export { | ||
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 @@ | |||
1 | export * from './account' | ||
1 | export * from './oembed' | 2 | export * from './oembed' |
2 | export * from './remote' | 3 | export * from './activitypub' |
3 | export * from './pagination' | 4 | export * from './pagination' |
4 | export * from './pods' | 5 | export * from './pods' |
5 | export * from './sort' | 6 | export * from './sort' |
diff --git a/server/middlewares/validators/remote/signature.ts b/server/middlewares/validators/remote/signature.ts deleted file mode 100644 index d3937b515..000000000 --- a/server/middlewares/validators/remote/signature.ts +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | import { body } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { logger, isHostValid } from '../../../helpers' | ||
5 | import { checkErrors } from '../utils' | ||
6 | |||
7 | const 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 | |||
20 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | |||
6 | export namespace AccountFollowMethods { | ||
7 | } | ||
8 | |||
9 | export interface AccountFollowClass { | ||
10 | } | ||
11 | |||
12 | export interface AccountFollowAttributes { | ||
13 | accountId: number | ||
14 | targetAccountId: number | ||
15 | } | ||
16 | |||
17 | export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> { | ||
18 | id: number | ||
19 | createdAt: Date | ||
20 | updatedAt: Date | ||
21 | } | ||
22 | |||
23 | export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {} | ||
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts new file mode 100644 index 000000000..9bf03b253 --- /dev/null +++ b/server/models/account/account-follow.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { addMethodsToModel } from '../utils' | ||
4 | import { | ||
5 | AccountFollowInstance, | ||
6 | AccountFollowAttributes, | ||
7 | |||
8 | AccountFollowMethods | ||
9 | } from './account-follow-interface' | ||
10 | |||
11 | let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> | ||
12 | |||
13 | export 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 | |||
40 | function 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | |||
4 | import { PodInstance } from '../pod/pod-interface' | ||
5 | import { VideoChannelInstance } from '../video/video-channel-interface' | ||
6 | import { ActivityPubActor } from '../../../shared' | ||
7 | import { ResultList } from '../../../shared/models/result-list.model' | ||
8 | |||
9 | export 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 | |||
27 | export 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 | |||
38 | export 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 | |||
58 | export 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 | |||
74 | export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {} | ||
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts new file mode 100644 index 000000000..82cbe38cc --- /dev/null +++ b/server/models/account/account-video-rate-interface.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | |||
6 | export namespace AccountVideoRateMethods { | ||
7 | export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance> | ||
8 | } | ||
9 | |||
10 | export interface AccountVideoRateClass { | ||
11 | load: AccountVideoRateMethods.Load | ||
12 | } | ||
13 | |||
14 | export interface AccountVideoRateAttributes { | ||
15 | type: VideoRateType | ||
16 | accountId: number | ||
17 | videoId: number | ||
18 | } | ||
19 | |||
20 | export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> { | ||
21 | id: number | ||
22 | createdAt: Date | ||
23 | updatedAt: Date | ||
24 | } | ||
25 | |||
26 | export 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 | */ |
4 | import { values } from 'lodash' | 4 | import { values } from 'lodash' |
5 | import * as Sequelize from 'sequelize' | 5 | import * as Sequelize from 'sequelize' |
@@ -8,17 +8,17 @@ import { VIDEO_RATE_TYPES } from '../../initializers' | |||
8 | 8 | ||
9 | import { addMethodsToModel } from '../utils' | 9 | import { addMethodsToModel } from '../utils' |
10 | import { | 10 | import { |
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 | ||
17 | let UserVideoRate: Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> | 17 | let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> |
18 | let load: UserVideoRateMethods.Load | 18 | let load: AccountVideoRateMethods.Load |
19 | 19 | ||
20 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 20 | export 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 | ||
50 | function associate (models) { | 50 | function 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 | ||
68 | load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) { | 68 | load = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { | ||
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 | |||
18 | import { addMethodsToModel } from '../utils' | ||
19 | import { | ||
20 | AccountInstance, | ||
21 | AccountAttributes, | ||
22 | |||
23 | AccountMethods | ||
24 | } from './account-interface' | ||
25 | |||
26 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | ||
27 | let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID | ||
28 | let load: AccountMethods.Load | ||
29 | let loadByUUID: AccountMethods.LoadByUUID | ||
30 | let loadByUrl: AccountMethods.LoadByUrl | ||
31 | let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName | ||
32 | let listOwned: AccountMethods.ListOwned | ||
33 | let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi | ||
34 | let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi | ||
35 | let isOwned: AccountMethods.IsOwned | ||
36 | let toActivityPubObject: AccountMethods.ToActivityPubObject | ||
37 | let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls | ||
38 | let getFollowingUrl: AccountMethods.GetFollowingUrl | ||
39 | let getFollowersUrl: AccountMethods.GetFollowersUrl | ||
40 | let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl | ||
41 | |||
42 | export 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 | |||
214 | function 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 | |||
265 | function 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 | |||
277 | toActivityPubObject = 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 | |||
304 | isOwned = function (this: AccountInstance) { | ||
305 | return this.podId === null | ||
306 | } | ||
307 | |||
308 | getFollowerSharedInboxUrls = 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 | |||
325 | getFollowingUrl = function (this: AccountInstance) { | ||
326 | return this.url + '/followers' | ||
327 | } | ||
328 | |||
329 | getFollowersUrl = function (this: AccountInstance) { | ||
330 | return this.url + '/followers' | ||
331 | } | ||
332 | |||
333 | getPublicKeyUrl = function (this: AccountInstance) { | ||
334 | return this.url + '#main-key' | ||
335 | } | ||
336 | |||
337 | // ------------------------------ STATICS ------------------------------ | ||
338 | |||
339 | listOwned = function () { | ||
340 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
341 | where: { | ||
342 | podId: null | ||
343 | } | ||
344 | } | ||
345 | |||
346 | return Account.findAll(query) | ||
347 | } | ||
348 | |||
349 | listFollowerUrlsForApi = function (name: string, start: number, count: number) { | ||
350 | return createListFollowForApiQuery('followers', name, start, count) | ||
351 | } | ||
352 | |||
353 | listFollowingUrlsForApi = function (name: string, start: number, count: number) { | ||
354 | return createListFollowForApiQuery('following', name, start, count) | ||
355 | } | ||
356 | |||
357 | load = function (id: number) { | ||
358 | return Account.findById(id) | ||
359 | } | ||
360 | |||
361 | loadByUUID = function (uuid: string) { | ||
362 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
363 | where: { | ||
364 | uuid | ||
365 | } | ||
366 | } | ||
367 | |||
368 | return Account.findOne(query) | ||
369 | } | ||
370 | |||
371 | loadLocalAccountByName = 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 | |||
384 | loadByUrl = function (url: string) { | ||
385 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
386 | where: { | ||
387 | url | ||
388 | } | ||
389 | } | ||
390 | |||
391 | return Account.findOne(query) | ||
392 | } | ||
393 | |||
394 | loadAccountByPodAndUUID = 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 | |||
408 | async 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 @@ | |||
1 | export * from './account-interface' | ||
2 | export * from './account-follow-interface' | ||
3 | export * from './account-video-rate-interface' | ||
4 | export * 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | 2 | import * 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 |
5 | import { AccountInstance } from './account-interface' | ||
5 | import { User as FormattedUser } from '../../../shared/models/users/user.model' | 6 | import { User as FormattedUser } from '../../../shared/models/users/user.model' |
6 | import { ResultList } from '../../../shared/models/result-list.model' | 7 | import { ResultList } from '../../../shared/models/result-list.model' |
7 | import { AuthorInstance } from '../video/author-interface' | ||
8 | import { UserRight } from '../../../shared/models/users/user-right.enum' | 8 | import { UserRight } from '../../../shared/models/users/user-right.enum' |
9 | import { UserRole } from '../../../shared/models/users/user-role' | 9 | import { 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 | ||
32 | export interface UserClass { | 32 | export 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 | ||
59 | export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { | 59 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | ||
3 | 2 | ||
4 | import { getSort, addMethodsToModel } from '../utils' | 3 | import { getSort, addMethodsToModel } from '../utils' |
5 | import { | 4 | import { |
@@ -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 | ||
200 | function associate (models) { | 199 | function 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 | ||
243 | loadById = function (id: number) { | 242 | loadById = 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 | ||
279 | loadByUsernameOrEmail = function (username: string, email: string) { | 278 | loadByUsernameOrEmail = 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' | |||
3 | export * from './oauth' | 3 | export * from './oauth' |
4 | export * from './pod' | 4 | export * from './pod' |
5 | export * from './request' | 5 | export * from './request' |
6 | export * from './user' | 6 | export * from './account' |
7 | export * from './video' | 7 | export * from './video' |
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts index ba5622977..163930a4f 100644 --- a/server/models/job/job-interface.ts +++ b/server/models/job/job-interface.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | 3 | ||
4 | import { JobState } from '../../../shared/models/job.model' | 4 | import { JobCategory, JobState } from '../../../shared/models/job.model' |
5 | 5 | ||
6 | export namespace JobMethods { | 6 | export 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 | ||
10 | export interface JobClass { | 10 | export interface JobClass { |
11 | listWithLimit: JobMethods.ListWithLimit | 11 | listWithLimitByCategory: JobMethods.ListWithLimitByCategory |
12 | } | 12 | } |
13 | 13 | ||
14 | export interface JobAttributes { | 14 | export interface JobAttributes { |
diff --git a/server/models/job/job.ts b/server/models/job/job.ts index 968f9d71d..ce1203e5a 100644 --- a/server/models/job/job.ts +++ b/server/models/job/job.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | 3 | ||
4 | import { JOB_STATES } from '../../initializers' | 4 | import { JOB_STATES, JOB_CATEGORIES } from '../../initializers' |
5 | 5 | ||
6 | import { addMethodsToModel } from '../utils' | 6 | import { addMethodsToModel } from '../utils' |
7 | import { | 7 | import { |
@@ -13,7 +13,7 @@ import { | |||
13 | import { JobState } from '../../../shared/models/job.model' | 13 | import { JobState } from '../../../shared/models/job.model' |
14 | 14 | ||
15 | let Job: Sequelize.Model<JobInstance, JobAttributes> | 15 | let Job: Sequelize.Model<JobInstance, JobAttributes> |
16 | let listWithLimit: JobMethods.ListWithLimit | 16 | let listWithLimitByCategory: JobMethods.ListWithLimitByCategory |
17 | 17 | ||
18 | export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 18 | export 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 | ||
51 | listWithLimit = function (limit: number, state: JobState) { | 55 | listWithLimitByCategory = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | 3 | ||
4 | import { UserModel } from '../user/user-interface' | 4 | import { UserModel } from '../account/user-interface' |
5 | 5 | ||
6 | export type OAuthTokenInfo = { | 6 | export 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 { | |||
48 | export interface PodAttributes { | 48 | export 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 | ||
56 | export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance<PodAttributes> { | 54 | export 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 @@ | |||
1 | export * from './user-video-rate-interface' | ||
2 | export * from './user-interface' | ||
diff --git a/server/models/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts deleted file mode 100644 index ea0fdc4d9..000000000 --- a/server/models/user/user-video-rate-interface.ts +++ /dev/null | |||
@@ -1,26 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | |||
6 | export namespace UserVideoRateMethods { | ||
7 | export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<UserVideoRateInstance> | ||
8 | } | ||
9 | |||
10 | export interface UserVideoRateClass { | ||
11 | load: UserVideoRateMethods.Load | ||
12 | } | ||
13 | |||
14 | export interface UserVideoRateAttributes { | ||
15 | type: VideoRateType | ||
16 | userId: number | ||
17 | videoId: number | ||
18 | } | ||
19 | |||
20 | export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance<UserVideoRateAttributes> { | ||
21 | id: number | ||
22 | createdAt: Date | ||
23 | updatedAt: Date | ||
24 | } | ||
25 | |||
26 | export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model<UserVideoRateInstance, UserVideoRateAttributes> {} | ||
diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts deleted file mode 100644 index fc69ff3c2..000000000 --- a/server/models/video/author-interface.ts +++ /dev/null | |||
@@ -1,45 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { PodInstance } from '../pod/pod-interface' | ||
5 | import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model' | ||
6 | import { VideoChannelInstance } from './video-channel-interface' | ||
7 | |||
8 | export 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 | |||
18 | export interface AuthorClass { | ||
19 | loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID | ||
20 | load: AuthorMethods.Load | ||
21 | loadByUUID: AuthorMethods.LoadByUUID | ||
22 | listOwned: AuthorMethods.ListOwned | ||
23 | } | ||
24 | |||
25 | export interface AuthorAttributes { | ||
26 | name: string | ||
27 | uuid?: string | ||
28 | |||
29 | podId?: number | ||
30 | userId?: number | ||
31 | } | ||
32 | |||
33 | export 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 | |||
45 | export interface AuthorModel extends AuthorClass, Sequelize.Model<AuthorInstance, AuthorAttributes> {} | ||
diff --git a/server/models/video/author.ts b/server/models/video/author.ts deleted file mode 100644 index 43f84c3ea..000000000 --- a/server/models/video/author.ts +++ /dev/null | |||
@@ -1,171 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { isUserUsernameValid } from '../../helpers' | ||
4 | import { removeVideoAuthorToFriends } from '../../lib' | ||
5 | |||
6 | import { addMethodsToModel } from '../utils' | ||
7 | import { | ||
8 | AuthorInstance, | ||
9 | AuthorAttributes, | ||
10 | |||
11 | AuthorMethods | ||
12 | } from './author-interface' | ||
13 | |||
14 | let Author: Sequelize.Model<AuthorInstance, AuthorAttributes> | ||
15 | let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID | ||
16 | let load: AuthorMethods.Load | ||
17 | let loadByUUID: AuthorMethods.LoadByUUID | ||
18 | let listOwned: AuthorMethods.ListOwned | ||
19 | let isOwned: AuthorMethods.IsOwned | ||
20 | let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON | ||
21 | |||
22 | export 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 | |||
83 | function 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 | |||
110 | function 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 | |||
122 | toAddRemoteJSON = function (this: AuthorInstance) { | ||
123 | const json = { | ||
124 | uuid: this.uuid, | ||
125 | name: this.name | ||
126 | } | ||
127 | |||
128 | return json | ||
129 | } | ||
130 | |||
131 | isOwned = function (this: AuthorInstance) { | ||
132 | return this.podId === null | ||
133 | } | ||
134 | |||
135 | // ------------------------------ STATICS ------------------------------ | ||
136 | |||
137 | listOwned = function () { | ||
138 | const query: Sequelize.FindOptions<AuthorAttributes> = { | ||
139 | where: { | ||
140 | podId: null | ||
141 | } | ||
142 | } | ||
143 | |||
144 | return Author.findAll(query) | ||
145 | } | ||
146 | |||
147 | load = function (id: number) { | ||
148 | return Author.findById(id) | ||
149 | } | ||
150 | |||
151 | loadByUUID = function (uuid: string) { | ||
152 | const query: Sequelize.FindOptions<AuthorAttributes> = { | ||
153 | where: { | ||
154 | uuid | ||
155 | } | ||
156 | } | ||
157 | |||
158 | return Author.findOne(query) | ||
159 | } | ||
160 | |||
161 | loadAuthorByPodAndUUID = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
3 | 3 | ||
4 | import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' | 4 | import { 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 |
7 | import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' | 7 | import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' |
8 | import { AuthorInstance } from './author-interface' | ||
9 | import { VideoInstance } from './video-interface' | 8 | import { VideoInstance } from './video-interface' |
9 | import { AccountInstance } from '../account/account-interface' | ||
10 | import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' | ||
10 | 11 | ||
11 | export namespace VideoChannelMethods { | 12 | export 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 | ||
29 | export interface VideoChannelClass { | 29 | export 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 | ||
42 | export interface VideoChannelAttributes { | 42 | export 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 | ||
64 | export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {} | 64 | export 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 | ||
14 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> | 14 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> |
15 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON | 15 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON |
16 | let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON | 16 | let toActivityPubObject: VideoChannelMethods.ToActivityPubObject |
17 | let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON | ||
18 | let isOwned: VideoChannelMethods.IsOwned | 17 | let isOwned: VideoChannelMethods.IsOwned |
19 | let countByAuthor: VideoChannelMethods.CountByAuthor | 18 | let countByAccount: VideoChannelMethods.CountByAccount |
20 | let listOwned: VideoChannelMethods.ListOwned | 19 | let listOwned: VideoChannelMethods.ListOwned |
21 | let listForApi: VideoChannelMethods.ListForApi | 20 | let listForApi: VideoChannelMethods.ListForApi |
22 | let listByAuthor: VideoChannelMethods.ListByAuthor | 21 | let listByAccount: VideoChannelMethods.ListByAccount |
23 | let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor | 22 | let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount |
24 | let loadByUUID: VideoChannelMethods.LoadByUUID | 23 | let loadByUUID: VideoChannelMethods.LoadByUUID |
25 | let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor | 24 | let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount |
26 | let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor | 25 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount |
27 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | 26 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID |
28 | let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos | 27 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos |
29 | 28 | ||
30 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 29 | export 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 | ||
135 | toAddRemoteJSON = function (this: VideoChannelInstance) { | 140 | toActivityPubObject = 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 | |||
148 | toUpdateRemoteJSON = 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 | ||
163 | function associate (models) { | 155 | function 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 | ||
193 | countByAuthor = function (authorId: number) { | 185 | countByAccount = 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 | ||
233 | listByAuthor = function (authorId: number) { | 225 | listByAccount = 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 | ||
291 | loadByIdAndAuthor = function (id: number, authorId: number) { | 283 | loadByIdAndAccount = 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 | ||
308 | loadAndPopulateAuthor = function (id: number) { | 300 | loadAndPopulateAccount = 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 | ||
321 | loadByUUIDAndPopulateAuthor = function (uuid: string) { | 313 | loadByUUIDAndPopulateAccount = 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 | ||
337 | loadAndPopulateAuthorAndVideos = function (id: number) { | 329 | loadAndPopulateAccountAndVideos = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as Promise from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | 3 | ||
4 | import { TagAttributes, TagInstance } from './tag-interface' | 4 | import { TagAttributes, TagInstance } from './tag-interface' |
5 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | 5 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' |
@@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/ | |||
13 | import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' | 13 | import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' |
14 | import { ResultList } from '../../../shared/models/result-list.model' | 14 | import { ResultList } from '../../../shared/models/result-list.model' |
15 | import { VideoChannelInstance } from './video-channel-interface' | 15 | import { VideoChannelInstance } from './video-channel-interface' |
16 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | ||
16 | 17 | ||
17 | export namespace VideoMethods { | 18 | export 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 | ||
92 | export interface VideoAttributes { | 97 | export 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 | ||
151 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} | 160 | export 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' | |||
5 | import * as parseTorrent from 'parse-torrent' | 5 | import * as parseTorrent from 'parse-torrent' |
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import * as Sequelize from 'sequelize' | 7 | import * as Sequelize from 'sequelize' |
8 | import * as Promise from 'bluebird' | ||
9 | 8 | ||
10 | import { TagInstance } from './tag-interface' | 9 | import { TagInstance } from './tag-interface' |
11 | import { | 10 | import { |
@@ -52,6 +51,7 @@ import { | |||
52 | 51 | ||
53 | VideoMethods | 52 | VideoMethods |
54 | } from './video-interface' | 53 | } from './video-interface' |
54 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | ||
55 | 55 | ||
56 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | 56 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> |
57 | let getOriginalFile: VideoMethods.GetOriginalFile | 57 | let getOriginalFile: VideoMethods.GetOriginalFile |
@@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName | |||
64 | let isOwned: VideoMethods.IsOwned | 64 | let isOwned: VideoMethods.IsOwned |
65 | let toFormattedJSON: VideoMethods.ToFormattedJSON | 65 | let toFormattedJSON: VideoMethods.ToFormattedJSON |
66 | let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON | 66 | let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON |
67 | let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON | 67 | let toActivityPubObject: VideoMethods.ToActivityPubObject |
68 | let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON | ||
69 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile | 68 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile |
70 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | 69 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile |
71 | let createPreview: VideoMethods.CreatePreview | 70 | let createPreview: VideoMethods.CreatePreview |
@@ -76,21 +75,25 @@ let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | |||
76 | let getEmbedPath: VideoMethods.GetEmbedPath | 75 | let getEmbedPath: VideoMethods.GetEmbedPath |
77 | let getDescriptionPath: VideoMethods.GetDescriptionPath | 76 | let getDescriptionPath: VideoMethods.GetDescriptionPath |
78 | let getTruncatedDescription: VideoMethods.GetTruncatedDescription | 77 | let getTruncatedDescription: VideoMethods.GetTruncatedDescription |
78 | let getCategoryLabel: VideoMethods.GetCategoryLabel | ||
79 | let getLicenceLabel: VideoMethods.GetLicenceLabel | ||
80 | let getLanguageLabel: VideoMethods.GetLanguageLabel | ||
79 | 81 | ||
80 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData | 82 | let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData |
81 | let list: VideoMethods.List | 83 | let list: VideoMethods.List |
82 | let listForApi: VideoMethods.ListForApi | 84 | let listForApi: VideoMethods.ListForApi |
83 | let listUserVideosForApi: VideoMethods.ListUserVideosForApi | 85 | let listUserVideosForApi: VideoMethods.ListUserVideosForApi |
84 | let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID | 86 | let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID |
85 | let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags | 87 | let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags |
86 | let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor | 88 | let listOwnedByAccount: VideoMethods.ListOwnedByAccount |
87 | let load: VideoMethods.Load | 89 | let load: VideoMethods.Load |
88 | let loadByUUID: VideoMethods.LoadByUUID | 90 | let loadByUUID: VideoMethods.LoadByUUID |
91 | let loadByUrl: VideoMethods.LoadByUrl | ||
89 | let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID | 92 | let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID |
90 | let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor | 93 | let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount |
91 | let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags | 94 | let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags |
92 | let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags | 95 | let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags |
93 | let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags | 96 | let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags |
94 | let removeThumbnail: VideoMethods.RemoveThumbnail | 97 | let removeThumbnail: VideoMethods.RemoveThumbnail |
95 | let removePreview: VideoMethods.RemovePreview | 98 | let removePreview: VideoMethods.RemovePreview |
96 | let removeFile: VideoMethods.RemoveFile | 99 | let 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 | ||
426 | createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { | 449 | createTorrentAndSetInfoHash = 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 | ||
450 | getEmbedPath = function (this: VideoInstance) { | 470 | getEmbedPath = function (this: VideoInstance) { |
@@ -462,40 +482,28 @@ getPreviewPath = function (this: VideoInstance) { | |||
462 | toFormattedJSON = function (this: VideoInstance) { | 482 | toFormattedJSON = 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 | ||
555 | toAddRemoteJSON = function (this: VideoInstance) { | 563 | toActivityPubObject = 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 | ||
594 | toUpdateRemoteJSON = 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 | ||
626 | getTruncatedDescription = function (this: VideoInstance) { | 634 | getTruncatedDescription = 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 | ||
634 | optimizeOriginalVideofile = function (this: VideoInstance) { | 642 | optimizeOriginalVideofile = 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 | ||
679 | transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { | 679 | transcodeOriginalVideofile = 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 | ||
720 | getOriginalFileHeight = function (this: VideoInstance) { | 713 | getOriginalFileHeight = 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 | ||
723 | getCategoryLabel = 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 | |||
732 | getLicenceLabel = 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 | |||
740 | getLanguageLabel = 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 | |||
730 | removeThumbnail = function (this: VideoInstance) { | 748 | removeThumbnail = 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 | ||
870 | listOwnedAndPopulateAuthorAndTags = function () { | 888 | listOwnedAndPopulateAccountAndTags = 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 | ||
888 | listOwnedByAuthor = function (author: string) { | 906 | listOwnedByAccount = 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 | ||
945 | loadAndPopulateAuthor = function (id: number) { | 963 | loadAndPopulateAccount = 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 | ||
959 | loadAndPopulateAuthorAndPodAndTags = function (id: number) { | 977 | loadAndPopulateAccountAndPodAndTags = 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 | ||
979 | loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { | 997 | loadByUUIDAndPopulateAccountAndPodAndTags = 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 | ||
1002 | searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { | 1020 | searchAndPopulateAccountAndPodAndTags = 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 | ||
1118 | function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) { | ||
1119 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName() | ||
1120 | } | ||
1121 | |||
1100 | function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { | 1122 | function 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 | } |