diff options
Diffstat (limited to 'server')
111 files changed, 4438 insertions, 415 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 6229c44aa..433186179 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -13,8 +13,7 @@ import { | |||
13 | localVideoChannelValidator, | 13 | localVideoChannelValidator, |
14 | videosCustomGetValidator | 14 | videosCustomGetValidator |
15 | } from '../../middlewares' | 15 | } from '../../middlewares' |
16 | import { videosGetValidator, videosShareValidator } from '../../middlewares/validators' | 16 | import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' |
17 | import { videoCommentGetValidator } from '../../middlewares/validators/video-comments' | ||
18 | import { AccountModel } from '../../models/account/account' | 17 | import { AccountModel } from '../../models/account/account' |
19 | import { ActorModel } from '../../models/activitypub/actor' | 18 | import { ActorModel } from '../../models/activitypub/actor' |
20 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 19 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index b7691ccba..86ef2aed1 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getFormattedObjects } from '../../helpers/utils' | 2 | import { getFormattedObjects } from '../../helpers/utils' |
3 | import { | 3 | import { |
4 | asyncMiddleware, commonVideosFiltersValidator, | 4 | asyncMiddleware, |
5 | commonVideosFiltersValidator, | ||
5 | listVideoAccountChannelsValidator, | 6 | listVideoAccountChannelsValidator, |
6 | optionalAuthenticate, | 7 | optionalAuthenticate, |
7 | paginationValidator, | 8 | paginationValidator, |
@@ -86,9 +87,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n | |||
86 | languageOneOf: req.query.languageOneOf, | 87 | languageOneOf: req.query.languageOneOf, |
87 | tagsOneOf: req.query.tagsOneOf, | 88 | tagsOneOf: req.query.tagsOneOf, |
88 | tagsAllOf: req.query.tagsAllOf, | 89 | tagsAllOf: req.query.tagsAllOf, |
90 | filter: req.query.filter, | ||
89 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 91 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
90 | withFiles: false, | 92 | withFiles: false, |
91 | accountId: account.id | 93 | accountId: account.id, |
94 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
92 | }) | 95 | }) |
93 | 96 | ||
94 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 97 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index fd4db7a54..534305ba6 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -117,7 +117,9 @@ function searchVideos (req: express.Request, res: express.Response) { | |||
117 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | 117 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { |
118 | const options = Object.assign(query, { | 118 | const options = Object.assign(query, { |
119 | includeLocalVideos: true, | 119 | includeLocalVideos: true, |
120 | nsfw: buildNSFWFilter(res, query.nsfw) | 120 | nsfw: buildNSFWFilter(res, query.nsfw), |
121 | filter: query.filter, | ||
122 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
121 | }) | 123 | }) |
122 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) | 124 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) |
123 | 125 | ||
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index d62400e42..9fa6c34ba 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -61,14 +61,26 @@ export { | |||
61 | 61 | ||
62 | async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { | 62 | async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { |
63 | const serverActor = await getServerActor() | 63 | const serverActor = await getServerActor() |
64 | const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) | 64 | const resultList = await ActorFollowModel.listFollowingForApi( |
65 | serverActor.id, | ||
66 | req.query.start, | ||
67 | req.query.count, | ||
68 | req.query.sort, | ||
69 | req.query.search | ||
70 | ) | ||
65 | 71 | ||
66 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 72 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
67 | } | 73 | } |
68 | 74 | ||
69 | async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { |
70 | const serverActor = await getServerActor() | 76 | const serverActor = await getServerActor() |
71 | const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) | 77 | const resultList = await ActorFollowModel.listFollowersForApi( |
78 | serverActor.id, | ||
79 | req.query.start, | ||
80 | req.query.count, | ||
81 | req.query.sort, | ||
82 | req.query.search | ||
83 | ) | ||
72 | 84 | ||
73 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 85 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
74 | } | 86 | } |
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts index 43bca2c10..c08192a8c 100644 --- a/server/controllers/api/server/index.ts +++ b/server/controllers/api/server/index.ts | |||
@@ -2,12 +2,14 @@ import * as express from 'express' | |||
2 | import { serverFollowsRouter } from './follows' | 2 | import { serverFollowsRouter } from './follows' |
3 | import { statsRouter } from './stats' | 3 | import { statsRouter } from './stats' |
4 | import { serverRedundancyRouter } from './redundancy' | 4 | import { serverRedundancyRouter } from './redundancy' |
5 | import { serverBlocklistRouter } from './server-blocklist' | ||
5 | 6 | ||
6 | const serverRouter = express.Router() | 7 | const serverRouter = express.Router() |
7 | 8 | ||
8 | serverRouter.use('/', serverFollowsRouter) | 9 | serverRouter.use('/', serverFollowsRouter) |
9 | serverRouter.use('/', serverRedundancyRouter) | 10 | serverRouter.use('/', serverRedundancyRouter) |
10 | serverRouter.use('/', statsRouter) | 11 | serverRouter.use('/', statsRouter) |
12 | serverRouter.use('/', serverBlocklistRouter) | ||
11 | 13 | ||
12 | // --------------------------------------------------------------------------- | 14 | // --------------------------------------------------------------------------- |
13 | 15 | ||
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts new file mode 100644 index 000000000..3cb3a96e2 --- /dev/null +++ b/server/controllers/api/server/server-blocklist.ts | |||
@@ -0,0 +1,132 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | asyncRetryTransactionMiddleware, | ||
7 | authenticate, | ||
8 | ensureUserHasRight, | ||
9 | paginationValidator, | ||
10 | setDefaultPagination, | ||
11 | setDefaultSort | ||
12 | } from '../../../middlewares' | ||
13 | import { | ||
14 | accountsBlocklistSortValidator, | ||
15 | blockAccountValidator, | ||
16 | blockServerValidator, | ||
17 | serversBlocklistSortValidator, | ||
18 | unblockAccountByServerValidator, | ||
19 | unblockServerByServerValidator | ||
20 | } from '../../../middlewares/validators' | ||
21 | import { AccountModel } from '../../../models/account/account' | ||
22 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | ||
23 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
24 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | ||
25 | import { ServerModel } from '../../../models/server/server' | ||
26 | import { UserRight } from '../../../../shared/models/users' | ||
27 | |||
28 | const serverBlocklistRouter = express.Router() | ||
29 | |||
30 | serverBlocklistRouter.get('/blocklist/accounts', | ||
31 | authenticate, | ||
32 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
33 | paginationValidator, | ||
34 | accountsBlocklistSortValidator, | ||
35 | setDefaultSort, | ||
36 | setDefaultPagination, | ||
37 | asyncMiddleware(listBlockedAccounts) | ||
38 | ) | ||
39 | |||
40 | serverBlocklistRouter.post('/blocklist/accounts', | ||
41 | authenticate, | ||
42 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
43 | asyncMiddleware(blockAccountValidator), | ||
44 | asyncRetryTransactionMiddleware(blockAccount) | ||
45 | ) | ||
46 | |||
47 | serverBlocklistRouter.delete('/blocklist/accounts/:accountName', | ||
48 | authenticate, | ||
49 | ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), | ||
50 | asyncMiddleware(unblockAccountByServerValidator), | ||
51 | asyncRetryTransactionMiddleware(unblockAccount) | ||
52 | ) | ||
53 | |||
54 | serverBlocklistRouter.get('/blocklist/servers', | ||
55 | authenticate, | ||
56 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
57 | paginationValidator, | ||
58 | serversBlocklistSortValidator, | ||
59 | setDefaultSort, | ||
60 | setDefaultPagination, | ||
61 | asyncMiddleware(listBlockedServers) | ||
62 | ) | ||
63 | |||
64 | serverBlocklistRouter.post('/blocklist/servers', | ||
65 | authenticate, | ||
66 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
67 | asyncMiddleware(blockServerValidator), | ||
68 | asyncRetryTransactionMiddleware(blockServer) | ||
69 | ) | ||
70 | |||
71 | serverBlocklistRouter.delete('/blocklist/servers/:host', | ||
72 | authenticate, | ||
73 | ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), | ||
74 | asyncMiddleware(unblockServerByServerValidator), | ||
75 | asyncRetryTransactionMiddleware(unblockServer) | ||
76 | ) | ||
77 | |||
78 | export { | ||
79 | serverBlocklistRouter | ||
80 | } | ||
81 | |||
82 | // --------------------------------------------------------------------------- | ||
83 | |||
84 | async function listBlockedAccounts (req: express.Request, res: express.Response) { | ||
85 | const serverActor = await getServerActor() | ||
86 | |||
87 | const resultList = await AccountBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) | ||
88 | |||
89 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
90 | } | ||
91 | |||
92 | async function blockAccount (req: express.Request, res: express.Response) { | ||
93 | const serverActor = await getServerActor() | ||
94 | const accountToBlock: AccountModel = res.locals.account | ||
95 | |||
96 | await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) | ||
97 | |||
98 | return res.status(204).end() | ||
99 | } | ||
100 | |||
101 | async function unblockAccount (req: express.Request, res: express.Response) { | ||
102 | const accountBlock: AccountBlocklistModel = res.locals.accountBlock | ||
103 | |||
104 | await removeAccountFromBlocklist(accountBlock) | ||
105 | |||
106 | return res.status(204).end() | ||
107 | } | ||
108 | |||
109 | async function listBlockedServers (req: express.Request, res: express.Response) { | ||
110 | const serverActor = await getServerActor() | ||
111 | |||
112 | const resultList = await ServerBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) | ||
113 | |||
114 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
115 | } | ||
116 | |||
117 | async function blockServer (req: express.Request, res: express.Response) { | ||
118 | const serverActor = await getServerActor() | ||
119 | const serverToBlock: ServerModel = res.locals.server | ||
120 | |||
121 | await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) | ||
122 | |||
123 | return res.status(204).end() | ||
124 | } | ||
125 | |||
126 | async function unblockServer (req: express.Request, res: express.Response) { | ||
127 | const serverBlock: ServerBlocklistModel = res.locals.serverBlock | ||
128 | |||
129 | await removeServerFromBlocklist(serverBlock) | ||
130 | |||
131 | return res.status(204).end() | ||
132 | } | ||
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 0b0081520..9fcb8077f 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user' | |||
37 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 37 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
38 | import { meRouter } from './me' | 38 | import { meRouter } from './me' |
39 | import { deleteUserToken } from '../../../lib/oauth-model' | 39 | import { deleteUserToken } from '../../../lib/oauth-model' |
40 | import { myBlocklistRouter } from './my-blocklist' | ||
40 | 41 | ||
41 | const auditLogger = auditLoggerFactory('users') | 42 | const auditLogger = auditLoggerFactory('users') |
42 | 43 | ||
@@ -53,6 +54,7 @@ const askSendEmailLimiter = new RateLimit({ | |||
53 | }) | 54 | }) |
54 | 55 | ||
55 | const usersRouter = express.Router() | 56 | const usersRouter = express.Router() |
57 | usersRouter.use('/', myBlocklistRouter) | ||
56 | usersRouter.use('/', meRouter) | 58 | usersRouter.use('/', meRouter) |
57 | 59 | ||
58 | usersRouter.get('/autocomplete', | 60 | usersRouter.get('/autocomplete', |
@@ -238,7 +240,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response, n | |||
238 | } | 240 | } |
239 | 241 | ||
240 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 242 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { |
241 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) | 243 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) |
242 | 244 | ||
243 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 245 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
244 | } | 246 | } |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 591ec6b25..82299747d 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -238,7 +238,8 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res | |||
238 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 238 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
239 | filter: req.query.filter as VideoFilter, | 239 | filter: req.query.filter as VideoFilter, |
240 | withFiles: false, | 240 | withFiles: false, |
241 | actorId: user.Account.Actor.id | 241 | actorId: user.Account.Actor.id, |
242 | user | ||
242 | }) | 243 | }) |
243 | 244 | ||
244 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 245 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
@@ -327,6 +328,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
327 | if (body.password !== undefined) user.password = body.password | 328 | if (body.password !== undefined) user.password = body.password |
328 | if (body.email !== undefined) user.email = body.email | 329 | if (body.email !== undefined) user.email = body.email |
329 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 330 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy |
331 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | ||
330 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 332 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo |
331 | 333 | ||
332 | await sequelizeTypescript.transaction(async t => { | 334 | await sequelizeTypescript.transaction(async t => { |
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts new file mode 100644 index 000000000..9575eab46 --- /dev/null +++ b/server/controllers/api/users/my-blocklist.ts | |||
@@ -0,0 +1,125 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects } from '../../../helpers/utils' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | asyncRetryTransactionMiddleware, | ||
7 | authenticate, | ||
8 | paginationValidator, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort, | ||
11 | unblockAccountByAccountValidator | ||
12 | } from '../../../middlewares' | ||
13 | import { | ||
14 | accountsBlocklistSortValidator, | ||
15 | blockAccountValidator, | ||
16 | blockServerValidator, | ||
17 | serversBlocklistSortValidator, | ||
18 | unblockServerByAccountValidator | ||
19 | } from '../../../middlewares/validators' | ||
20 | import { UserModel } from '../../../models/account/user' | ||
21 | import { AccountModel } from '../../../models/account/account' | ||
22 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | ||
23 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
24 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | ||
25 | import { ServerModel } from '../../../models/server/server' | ||
26 | |||
27 | const myBlocklistRouter = express.Router() | ||
28 | |||
29 | myBlocklistRouter.get('/me/blocklist/accounts', | ||
30 | authenticate, | ||
31 | paginationValidator, | ||
32 | accountsBlocklistSortValidator, | ||
33 | setDefaultSort, | ||
34 | setDefaultPagination, | ||
35 | asyncMiddleware(listBlockedAccounts) | ||
36 | ) | ||
37 | |||
38 | myBlocklistRouter.post('/me/blocklist/accounts', | ||
39 | authenticate, | ||
40 | asyncMiddleware(blockAccountValidator), | ||
41 | asyncRetryTransactionMiddleware(blockAccount) | ||
42 | ) | ||
43 | |||
44 | myBlocklistRouter.delete('/me/blocklist/accounts/:accountName', | ||
45 | authenticate, | ||
46 | asyncMiddleware(unblockAccountByAccountValidator), | ||
47 | asyncRetryTransactionMiddleware(unblockAccount) | ||
48 | ) | ||
49 | |||
50 | myBlocklistRouter.get('/me/blocklist/servers', | ||
51 | authenticate, | ||
52 | paginationValidator, | ||
53 | serversBlocklistSortValidator, | ||
54 | setDefaultSort, | ||
55 | setDefaultPagination, | ||
56 | asyncMiddleware(listBlockedServers) | ||
57 | ) | ||
58 | |||
59 | myBlocklistRouter.post('/me/blocklist/servers', | ||
60 | authenticate, | ||
61 | asyncMiddleware(blockServerValidator), | ||
62 | asyncRetryTransactionMiddleware(blockServer) | ||
63 | ) | ||
64 | |||
65 | myBlocklistRouter.delete('/me/blocklist/servers/:host', | ||
66 | authenticate, | ||
67 | asyncMiddleware(unblockServerByAccountValidator), | ||
68 | asyncRetryTransactionMiddleware(unblockServer) | ||
69 | ) | ||
70 | |||
71 | export { | ||
72 | myBlocklistRouter | ||
73 | } | ||
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | async function listBlockedAccounts (req: express.Request, res: express.Response) { | ||
78 | const user: UserModel = res.locals.oauth.token.User | ||
79 | |||
80 | const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) | ||
81 | |||
82 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
83 | } | ||
84 | |||
85 | async function blockAccount (req: express.Request, res: express.Response) { | ||
86 | const user: UserModel = res.locals.oauth.token.User | ||
87 | const accountToBlock: AccountModel = res.locals.account | ||
88 | |||
89 | await addAccountInBlocklist(user.Account.id, accountToBlock.id) | ||
90 | |||
91 | return res.status(204).end() | ||
92 | } | ||
93 | |||
94 | async function unblockAccount (req: express.Request, res: express.Response) { | ||
95 | const accountBlock: AccountBlocklistModel = res.locals.accountBlock | ||
96 | |||
97 | await removeAccountFromBlocklist(accountBlock) | ||
98 | |||
99 | return res.status(204).end() | ||
100 | } | ||
101 | |||
102 | async function listBlockedServers (req: express.Request, res: express.Response) { | ||
103 | const user: UserModel = res.locals.oauth.token.User | ||
104 | |||
105 | const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) | ||
106 | |||
107 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
108 | } | ||
109 | |||
110 | async function blockServer (req: express.Request, res: express.Response) { | ||
111 | const user: UserModel = res.locals.oauth.token.User | ||
112 | const serverToBlock: ServerModel = res.locals.server | ||
113 | |||
114 | await addServerInBlocklist(user.Account.id, serverToBlock.id) | ||
115 | |||
116 | return res.status(204).end() | ||
117 | } | ||
118 | |||
119 | async function unblockServer (req: express.Request, res: express.Response) { | ||
120 | const serverBlock: ServerBlocklistModel = res.locals.serverBlock | ||
121 | |||
122 | await removeServerFromBlocklist(serverBlock) | ||
123 | |||
124 | return res.status(204).end() | ||
125 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 1fa842d9c..9bf3c5fd8 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -215,9 +215,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
215 | languageOneOf: req.query.languageOneOf, | 215 | languageOneOf: req.query.languageOneOf, |
216 | tagsOneOf: req.query.tagsOneOf, | 216 | tagsOneOf: req.query.tagsOneOf, |
217 | tagsAllOf: req.query.tagsAllOf, | 217 | tagsAllOf: req.query.tagsAllOf, |
218 | filter: req.query.filter, | ||
218 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 219 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
219 | withFiles: false, | 220 | withFiles: false, |
220 | videoChannelId: videoChannelInstance.id | 221 | videoChannelId: videoChannelInstance.id, |
222 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
221 | }) | 223 | }) |
222 | 224 | ||
223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 225 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts index 4cf8de1ef..3ba918189 100644 --- a/server/controllers/api/videos/captions.ts +++ b/server/controllers/api/videos/captions.ts | |||
@@ -1,10 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' | 2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' |
3 | import { | 3 | import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' |
4 | addVideoCaptionValidator, | ||
5 | deleteVideoCaptionValidator, | ||
6 | listVideoCaptionsValidator | ||
7 | } from '../../../middlewares/validators/video-captions' | ||
8 | import { createReqFiles } from '../../../helpers/express-utils' | 4 | import { createReqFiles } from '../../../helpers/express-utils' |
9 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' | 5 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' |
10 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index dc25e1e85..3875c8f79 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -8,19 +8,19 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide | |||
8 | import { | 8 | import { |
9 | asyncMiddleware, | 9 | asyncMiddleware, |
10 | asyncRetryTransactionMiddleware, | 10 | asyncRetryTransactionMiddleware, |
11 | authenticate, | 11 | authenticate, optionalAuthenticate, |
12 | paginationValidator, | 12 | paginationValidator, |
13 | setDefaultPagination, | 13 | setDefaultPagination, |
14 | setDefaultSort | 14 | setDefaultSort |
15 | } from '../../../middlewares' | 15 | } from '../../../middlewares' |
16 | import { videoCommentThreadsSortValidator } from '../../../middlewares/validators' | ||
17 | import { | 16 | import { |
18 | addVideoCommentReplyValidator, | 17 | addVideoCommentReplyValidator, |
19 | addVideoCommentThreadValidator, | 18 | addVideoCommentThreadValidator, |
20 | listVideoCommentThreadsValidator, | 19 | listVideoCommentThreadsValidator, |
21 | listVideoThreadCommentsValidator, | 20 | listVideoThreadCommentsValidator, |
22 | removeVideoCommentValidator | 21 | removeVideoCommentValidator, |
23 | } from '../../../middlewares/validators/video-comments' | 22 | videoCommentThreadsSortValidator |
23 | } from '../../../middlewares/validators' | ||
24 | import { VideoModel } from '../../../models/video/video' | 24 | import { VideoModel } from '../../../models/video/video' |
25 | import { VideoCommentModel } from '../../../models/video/video-comment' | 25 | import { VideoCommentModel } from '../../../models/video/video-comment' |
26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
@@ -36,10 +36,12 @@ videoCommentRouter.get('/:videoId/comment-threads', | |||
36 | setDefaultSort, | 36 | setDefaultSort, |
37 | setDefaultPagination, | 37 | setDefaultPagination, |
38 | asyncMiddleware(listVideoCommentThreadsValidator), | 38 | asyncMiddleware(listVideoCommentThreadsValidator), |
39 | optionalAuthenticate, | ||
39 | asyncMiddleware(listVideoThreads) | 40 | asyncMiddleware(listVideoThreads) |
40 | ) | 41 | ) |
41 | videoCommentRouter.get('/:videoId/comment-threads/:threadId', | 42 | videoCommentRouter.get('/:videoId/comment-threads/:threadId', |
42 | asyncMiddleware(listVideoThreadCommentsValidator), | 43 | asyncMiddleware(listVideoThreadCommentsValidator), |
44 | optionalAuthenticate, | ||
43 | asyncMiddleware(listVideoThreadComments) | 45 | asyncMiddleware(listVideoThreadComments) |
44 | ) | 46 | ) |
45 | 47 | ||
@@ -69,10 +71,12 @@ export { | |||
69 | 71 | ||
70 | async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { | 72 | async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { |
71 | const video = res.locals.video as VideoModel | 73 | const video = res.locals.video as VideoModel |
74 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
75 | |||
72 | let resultList: ResultList<VideoCommentModel> | 76 | let resultList: ResultList<VideoCommentModel> |
73 | 77 | ||
74 | if (video.commentsEnabled === true) { | 78 | if (video.commentsEnabled === true) { |
75 | resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort) | 79 | resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user) |
76 | } else { | 80 | } else { |
77 | resultList = { | 81 | resultList = { |
78 | total: 0, | 82 | total: 0, |
@@ -85,10 +89,12 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne | |||
85 | 89 | ||
86 | async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { | 90 | async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { |
87 | const video = res.locals.video as VideoModel | 91 | const video = res.locals.video as VideoModel |
92 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
93 | |||
88 | let resultList: ResultList<VideoCommentModel> | 94 | let resultList: ResultList<VideoCommentModel> |
89 | 95 | ||
90 | if (video.commentsEnabled === true) { | 96 | if (video.commentsEnabled === true) { |
91 | resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id) | 97 | resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user) |
92 | } else { | 98 | } else { |
93 | resultList = { | 99 | resultList = { |
94 | total: 0, | 100 | total: 0, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 15ef8d458..664154406 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -57,6 +57,7 @@ import { videoCaptionsRouter } from './captions' | |||
57 | import { videoImportsRouter } from './import' | 57 | import { videoImportsRouter } from './import' |
58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
59 | import { rename } from 'fs-extra' | 59 | import { rename } from 'fs-extra' |
60 | import { watchingRouter } from './watching' | ||
60 | 61 | ||
61 | const auditLogger = auditLoggerFactory('videos') | 62 | const auditLogger = auditLoggerFactory('videos') |
62 | const videosRouter = express.Router() | 63 | const videosRouter = express.Router() |
@@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter) | |||
86 | videosRouter.use('/', videoCaptionsRouter) | 87 | videosRouter.use('/', videoCaptionsRouter) |
87 | videosRouter.use('/', videoImportsRouter) | 88 | videosRouter.use('/', videoImportsRouter) |
88 | videosRouter.use('/', ownershipVideoRouter) | 89 | videosRouter.use('/', ownershipVideoRouter) |
90 | videosRouter.use('/', watchingRouter) | ||
89 | 91 | ||
90 | videosRouter.get('/categories', listVideoCategories) | 92 | videosRouter.get('/categories', listVideoCategories) |
91 | videosRouter.get('/licences', listVideoLicences) | 93 | videosRouter.get('/licences', listVideoLicences) |
@@ -119,6 +121,7 @@ videosRouter.get('/:id/description', | |||
119 | asyncMiddleware(getVideoDescription) | 121 | asyncMiddleware(getVideoDescription) |
120 | ) | 122 | ) |
121 | videosRouter.get('/:id', | 123 | videosRouter.get('/:id', |
124 | optionalAuthenticate, | ||
122 | asyncMiddleware(videosGetValidator), | 125 | asyncMiddleware(videosGetValidator), |
123 | getVideo | 126 | getVideo |
124 | ) | 127 | ) |
@@ -433,7 +436,8 @@ async function listVideos (req: express.Request, res: express.Response, next: ex | |||
433 | tagsAllOf: req.query.tagsAllOf, | 436 | tagsAllOf: req.query.tagsAllOf, |
434 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 437 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
435 | filter: req.query.filter as VideoFilter, | 438 | filter: req.query.filter as VideoFilter, |
436 | withFiles: false | 439 | withFiles: false, |
440 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
437 | }) | 441 | }) |
438 | 442 | ||
439 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 443 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts new file mode 100644 index 000000000..e8876b47a --- /dev/null +++ b/server/controllers/api/videos/watching.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import * as express from 'express' | ||
2 | import { UserWatchingVideo } from '../../../../shared' | ||
3 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' | ||
4 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | ||
5 | import { UserModel } from '../../../models/account/user' | ||
6 | |||
7 | const watchingRouter = express.Router() | ||
8 | |||
9 | watchingRouter.put('/:videoId/watching', | ||
10 | authenticate, | ||
11 | asyncMiddleware(videoWatchingValidator), | ||
12 | asyncRetryTransactionMiddleware(userWatchVideo) | ||
13 | ) | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | watchingRouter | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async function userWatchVideo (req: express.Request, res: express.Response) { | ||
24 | const user = res.locals.oauth.token.User as UserModel | ||
25 | |||
26 | const body: UserWatchingVideo = req.body | ||
27 | const { id: videoId } = res.locals.video as { id: number } | ||
28 | |||
29 | await UserVideoHistoryModel.upsert({ | ||
30 | videoId, | ||
31 | userId: user.id, | ||
32 | currentTime: body.currentTime | ||
33 | }) | ||
34 | |||
35 | return res.type('json').status(204).end() | ||
36 | } | ||
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index b30ad8e8d..ccb9b6029 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,7 +1,14 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' | 2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' |
3 | import { THUMBNAILS_SIZE } from '../initializers' | 3 | import { THUMBNAILS_SIZE } from '../initializers' |
4 | import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' | 4 | import { |
5 | asyncMiddleware, | ||
6 | commonVideosFiltersValidator, | ||
7 | setDefaultSort, | ||
8 | videoCommentsFeedsValidator, | ||
9 | videoFeedsValidator, | ||
10 | videosSortValidator | ||
11 | } from '../middlewares' | ||
5 | import { VideoModel } from '../models/video/video' | 12 | import { VideoModel } from '../models/video/video' |
6 | import * as Feed from 'pfeed' | 13 | import * as Feed from 'pfeed' |
7 | import { AccountModel } from '../models/account/account' | 14 | import { AccountModel } from '../models/account/account' |
@@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format', | |||
22 | videosSortValidator, | 29 | videosSortValidator, |
23 | setDefaultSort, | 30 | setDefaultSort, |
24 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), | 31 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), |
32 | commonVideosFiltersValidator, | ||
25 | asyncMiddleware(videoFeedsValidator), | 33 | asyncMiddleware(videoFeedsValidator), |
26 | asyncMiddleware(generateVideoFeed) | 34 | asyncMiddleware(generateVideoFeed) |
27 | ) | 35 | ) |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 224e4fe92..84e33c0e9 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -21,6 +21,7 @@ const timeTable = { | |||
21 | week: 3600000 * 24 * 7, | 21 | week: 3600000 * 24 * 7, |
22 | month: 3600000 * 24 * 30 | 22 | month: 3600000 * 24 * 30 |
23 | } | 23 | } |
24 | |||
24 | export function parseDuration (duration: number | string): number { | 25 | export function parseDuration (duration: number | string): number { |
25 | if (typeof duration === 'number') return duration | 26 | if (typeof duration === 'number') return duration |
26 | 27 | ||
@@ -41,6 +42,53 @@ export function parseDuration (duration: number | string): number { | |||
41 | throw new Error('Duration could not be properly parsed') | 42 | throw new Error('Duration could not be properly parsed') |
42 | } | 43 | } |
43 | 44 | ||
45 | export function parseBytes (value: string | number): number { | ||
46 | if (typeof value === 'number') return value | ||
47 | |||
48 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
49 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ | ||
50 | const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/ | ||
51 | const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
52 | const t = /^(\d+)\s*TB$/ | ||
53 | const g = /^(\d+)\s*GB$/ | ||
54 | const m = /^(\d+)\s*MB$/ | ||
55 | const b = /^(\d+)\s*B$/ | ||
56 | let match | ||
57 | |||
58 | if (value.match(tgm)) { | ||
59 | match = value.match(tgm) | ||
60 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
61 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
62 | + parseInt(match[3], 10) * 1024 * 1024 | ||
63 | } else if (value.match(tg)) { | ||
64 | match = value.match(tg) | ||
65 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
66 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
67 | } else if (value.match(tm)) { | ||
68 | match = value.match(tm) | ||
69 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
70 | + parseInt(match[2], 10) * 1024 * 1024 | ||
71 | } else if (value.match(gm)) { | ||
72 | match = value.match(gm) | ||
73 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
74 | + parseInt(match[2], 10) * 1024 * 1024 | ||
75 | } else if (value.match(t)) { | ||
76 | match = value.match(t) | ||
77 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
78 | } else if (value.match(g)) { | ||
79 | match = value.match(g) | ||
80 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
81 | } else if (value.match(m)) { | ||
82 | match = value.match(m) | ||
83 | return parseInt(match[1], 10) * 1024 * 1024 | ||
84 | } else if (value.match(b)) { | ||
85 | match = value.match(b) | ||
86 | return parseInt(match[1], 10) * 1024 | ||
87 | } else { | ||
88 | return parseInt(value, 10) | ||
89 | } | ||
90 | } | ||
91 | |||
44 | function sanitizeUrl (url: string) { | 92 | function sanitizeUrl (url: string) { |
45 | const urlObject = new URL(url) | 93 | const urlObject = new URL(url) |
46 | 94 | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index f88d26561..95fe824b9 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -81,19 +81,20 @@ function isRemoteVideoUrlValid (url: any) { | |||
81 | 81 | ||
82 | return url.type === 'Link' && | 82 | return url.type === 'Link' && |
83 | ( | 83 | ( |
84 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && | 84 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
85 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 && | ||
85 | isActivityPubUrlValid(url.href) && | 86 | isActivityPubUrlValid(url.href) && |
86 | validator.isInt(url.height + '', { min: 0 }) && | 87 | validator.isInt(url.height + '', { min: 0 }) && |
87 | validator.isInt(url.size + '', { min: 0 }) && | 88 | validator.isInt(url.size + '', { min: 0 }) && |
88 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) | 89 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) |
89 | ) || | 90 | ) || |
90 | ( | 91 | ( |
91 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && | 92 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 && |
92 | isActivityPubUrlValid(url.href) && | 93 | isActivityPubUrlValid(url.href) && |
93 | validator.isInt(url.height + '', { min: 0 }) | 94 | validator.isInt(url.height + '', { min: 0 }) |
94 | ) || | 95 | ) || |
95 | ( | 96 | ( |
96 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && | 97 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
97 | validator.isLength(url.href, { min: 5 }) && | 98 | validator.isLength(url.href, { min: 5 }) && |
98 | validator.isInt(url.height + '', { min: 0 }) | 99 | validator.isInt(url.height + '', { min: 0 }) |
99 | ) | 100 | ) |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 90fc74a48..1cb5e5b0f 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -42,6 +42,10 @@ function isUserNSFWPolicyValid (value: any) { | |||
42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | 42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 |
43 | } | 43 | } |
44 | 44 | ||
45 | function isUserWebTorrentEnabledValid (value: any) { | ||
46 | return isBooleanValid(value) | ||
47 | } | ||
48 | |||
45 | function isUserAutoPlayVideoValid (value: any) { | 49 | function isUserAutoPlayVideoValid (value: any) { |
46 | return isBooleanValid(value) | 50 | return isBooleanValid(value) |
47 | } | 51 | } |
@@ -78,6 +82,7 @@ export { | |||
78 | isUserUsernameValid, | 82 | isUserUsernameValid, |
79 | isUserEmailVerifiedValid, | 83 | isUserEmailVerifiedValid, |
80 | isUserNSFWPolicyValid, | 84 | isUserNSFWPolicyValid, |
85 | isUserWebTorrentEnabledValid, | ||
81 | isUserAutoPlayVideoValid, | 86 | isUserAutoPlayVideoValid, |
82 | isUserDisplayNameValid, | 87 | isUserDisplayNameValid, |
83 | isUserDescriptionValid, | 88 | isUserDescriptionValid, |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 9875c68bd..a13b09ac8 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -3,7 +3,7 @@ import 'express-validator' | |||
3 | import { values } from 'lodash' | 3 | import { values } from 'lodash' |
4 | import 'multer' | 4 | import 'multer' |
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' | 6 | import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
7 | import { | 7 | import { |
8 | CONSTRAINTS_FIELDS, | 8 | CONSTRAINTS_FIELDS, |
9 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
@@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video' | |||
22 | 22 | ||
23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
24 | 24 | ||
25 | function isVideoFilterValid (filter: VideoFilter) { | ||
26 | return filter === 'local' || filter === 'all-local' | ||
27 | } | ||
28 | |||
25 | function isVideoCategoryValid (value: any) { | 29 | function isVideoCategoryValid (value: any) { |
26 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined | 30 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined |
27 | } | 31 | } |
@@ -154,7 +158,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use | |||
154 | } | 158 | } |
155 | 159 | ||
156 | async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { | 160 | async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { |
157 | const video = await fetchVideo(id, fetchType) | 161 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
162 | |||
163 | const video = await fetchVideo(id, fetchType, userId) | ||
158 | 164 | ||
159 | if (video === null) { | 165 | if (video === null) { |
160 | res.status(404) | 166 | res.status(404) |
@@ -223,5 +229,6 @@ export { | |||
223 | isVideoExist, | 229 | isVideoExist, |
224 | isVideoImage, | 230 | isVideoImage, |
225 | isVideoChannelOfAccountExist, | 231 | isVideoChannelOfAccountExist, |
226 | isVideoSupportValid | 232 | isVideoSupportValid, |
233 | isVideoFilterValid | ||
227 | } | 234 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 8a9cee8c5..162fe2244 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,7 +2,6 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { User } from '../../shared/models/users' | ||
6 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAsync, generateRandomString } from './utils' |
7 | import { extname } from 'path' | 6 | import { extname } from 'path' |
8 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
@@ -101,7 +100,7 @@ function createReqFiles ( | |||
101 | } | 100 | } |
102 | 101 | ||
103 | function isUserAbleToSearchRemoteURI (res: express.Response) { | 102 | function isUserAbleToSearchRemoteURI (res: express.Response) { |
104 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | 103 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined |
105 | 104 | ||
106 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | 105 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || |
107 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | 106 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 22bc25476..8b9045038 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 55 | return 0 |
56 | } | 56 | } |
57 | 57 | ||
58 | async function getVideoFileBitrate (path: string) { | ||
59 | return new Promise<number>((res, rej) => { | ||
60 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
61 | if (err) return rej(err) | ||
62 | |||
63 | return res(metadata.format.bit_rate) | ||
64 | }) | ||
65 | }) | ||
66 | } | ||
67 | |||
58 | function getDurationFromVideoFile (path: string) { | 68 | function getDurationFromVideoFile (path: string) { |
59 | return new Promise<number>((res, rej) => { | 69 | return new Promise<number>((res, rej) => { |
60 | ffmpeg.ffprobe(path, (err, metadata) => { | 70 | ffmpeg.ffprobe(path, (err, metadata) => { |
@@ -106,45 +116,50 @@ type TranscodeOptions = { | |||
106 | 116 | ||
107 | function transcode (options: TranscodeOptions) { | 117 | function transcode (options: TranscodeOptions) { |
108 | return new Promise<void>(async (res, rej) => { | 118 | return new Promise<void>(async (res, rej) => { |
109 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 119 | try { |
110 | .output(options.outputPath) | 120 | let fps = await getVideoFileFPS(options.inputPath) |
111 | .preset(standard) | ||
112 | |||
113 | if (CONFIG.TRANSCODING.THREADS > 0) { | ||
114 | // if we don't set any threads ffmpeg will chose automatically | ||
115 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
116 | } | ||
117 | |||
118 | let fps = await getVideoFileFPS(options.inputPath) | ||
119 | if (options.resolution !== undefined) { | ||
120 | // '?x720' or '720x?' for example | ||
121 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
122 | command = command.size(size) | ||
123 | |||
124 | // On small/medium resolutions, limit FPS | 121 | // On small/medium resolutions, limit FPS |
125 | if ( | 122 | if ( |
123 | options.resolution !== undefined && | ||
126 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 124 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
127 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 125 | fps > VIDEO_TRANSCODING_FPS.AVERAGE |
128 | ) { | 126 | ) { |
129 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 127 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
130 | } | 128 | } |
131 | } | ||
132 | 129 | ||
133 | if (fps) { | 130 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
134 | // Hard FPS limits | 131 | .output(options.outputPath) |
135 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 132 | command = await presetH264(command, options.resolution, fps) |
136 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
137 | 133 | ||
138 | command = command.withFPS(fps) | 134 | if (CONFIG.TRANSCODING.THREADS > 0) { |
139 | } | 135 | // if we don't set any threads ffmpeg will chose automatically |
136 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
137 | } | ||
138 | |||
139 | if (options.resolution !== undefined) { | ||
140 | // '?x720' or '720x?' for example | ||
141 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
142 | command = command.size(size) | ||
143 | } | ||
144 | |||
145 | if (fps) { | ||
146 | // Hard FPS limits | ||
147 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | ||
148 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
140 | 149 | ||
141 | command | 150 | command = command.withFPS(fps) |
142 | .on('error', (err, stdout, stderr) => { | 151 | } |
143 | logger.error('Error in transcoding job.', { stdout, stderr }) | 152 | |
144 | return rej(err) | 153 | command |
145 | }) | 154 | .on('error', (err, stdout, stderr) => { |
146 | .on('end', res) | 155 | logger.error('Error in transcoding job.', { stdout, stderr }) |
147 | .run() | 156 | return rej(err) |
157 | }) | ||
158 | .on('end', res) | ||
159 | .run() | ||
160 | } catch (err) { | ||
161 | return rej(err) | ||
162 | } | ||
148 | }) | 163 | }) |
149 | } | 164 | } |
150 | 165 | ||
@@ -157,7 +172,8 @@ export { | |||
157 | transcode, | 172 | transcode, |
158 | getVideoFileFPS, | 173 | getVideoFileFPS, |
159 | computeResolutionsToTranscode, | 174 | computeResolutionsToTranscode, |
160 | audio | 175 | audio, |
176 | getVideoFileBitrate | ||
161 | } | 177 | } |
162 | 178 | ||
163 | // --------------------------------------------------------------------------- | 179 | // --------------------------------------------------------------------------- |
@@ -182,11 +198,10 @@ function getVideoFileStream (path: string) { | |||
182 | * and quality. Superfast and ultrafast will give you better | 198 | * and quality. Superfast and ultrafast will give you better |
183 | * performance, but then quality is noticeably worse. | 199 | * performance, but then quality is noticeably worse. |
184 | */ | 200 | */ |
185 | function veryfast (_ffmpeg) { | 201 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
186 | _ffmpeg | 202 | let localCommand = await presetH264(command, resolution, fps) |
187 | .preset(standard) | 203 | localCommand = localCommand.outputOption('-preset:v veryfast') |
188 | .outputOption('-preset:v veryfast') | 204 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) |
189 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | ||
190 | /* | 205 | /* |
191 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 206 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
192 | Our target situation is closer to a livestream than a stream, | 207 | Our target situation is closer to a livestream than a stream, |
@@ -198,31 +213,39 @@ function veryfast (_ffmpeg) { | |||
198 | Make up for most of the loss of grain and macroblocking | 213 | Make up for most of the loss of grain and macroblocking |
199 | with less computing power. | 214 | with less computing power. |
200 | */ | 215 | */ |
216 | |||
217 | return localCommand | ||
201 | } | 218 | } |
202 | 219 | ||
203 | /** | 220 | /** |
204 | * A preset optimised for a stillimage audio video | 221 | * A preset optimised for a stillimage audio video |
205 | */ | 222 | */ |
206 | function audio (_ffmpeg) { | 223 | async function presetStillImageWithAudio ( |
207 | _ffmpeg | 224 | command: ffmpeg.FfmpegCommand, |
208 | .preset(veryfast) | 225 | resolution: VideoResolution, |
209 | .outputOption('-tune stillimage') | 226 | fps: number |
227 | ): Promise<ffmpeg.FfmpegCommand> { | ||
228 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
229 | localCommand = localCommand.outputOption('-tune stillimage') | ||
230 | |||
231 | return localCommand | ||
210 | } | 232 | } |
211 | 233 | ||
212 | /** | 234 | /** |
213 | * A toolbox to play with audio | 235 | * A toolbox to play with audio |
214 | */ | 236 | */ |
215 | namespace audio { | 237 | namespace audio { |
216 | export const get = (_ffmpeg, pos: number | string = 0) => { | 238 | export const get = (option: ffmpeg.FfmpegCommand | string) => { |
217 | // without position, ffprobe considers the last input only | 239 | // without position, ffprobe considers the last input only |
218 | // we make it consider the first input only | 240 | // we make it consider the first input only |
219 | // if you pass a file path to pos, then ffprobe acts on that file directly | 241 | // if you pass a file path to pos, then ffprobe acts on that file directly |
220 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | 242 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { |
221 | _ffmpeg.ffprobe(pos, (err,data) => { | 243 | |
244 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
222 | if (err) return rej(err) | 245 | if (err) return rej(err) |
223 | 246 | ||
224 | if ('streams' in data) { | 247 | if ('streams' in data) { |
225 | const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') | 248 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') |
226 | if (audioStream) { | 249 | if (audioStream) { |
227 | return res({ | 250 | return res({ |
228 | absolutePath: data.format.filename, | 251 | absolutePath: data.format.filename, |
@@ -230,8 +253,15 @@ namespace audio { | |||
230 | }) | 253 | }) |
231 | } | 254 | } |
232 | } | 255 | } |
256 | |||
233 | return res({ absolutePath: data.format.filename }) | 257 | return res({ absolutePath: data.format.filename }) |
234 | }) | 258 | } |
259 | |||
260 | if (typeof option === 'string') { | ||
261 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
262 | } | ||
263 | |||
264 | return option.ffprobe(parseFfprobe) | ||
235 | }) | 265 | }) |
236 | } | 266 | } |
237 | 267 | ||
@@ -273,39 +303,48 @@ namespace audio { | |||
273 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 303 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
274 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 304 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
275 | */ | 305 | */ |
276 | async function standard (_ffmpeg) { | 306 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
277 | let localFfmpeg = _ffmpeg | 307 | let localCommand = command |
278 | .format('mp4') | 308 | .format('mp4') |
279 | .videoCodec('libx264') | 309 | .videoCodec('libx264') |
280 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | 310 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution |
281 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | 311 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it |
282 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | 312 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 |
313 | .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) | ||
283 | .outputOption('-map_metadata -1') // strip all metadata | 314 | .outputOption('-map_metadata -1') // strip all metadata |
284 | .outputOption('-movflags faststart') | 315 | .outputOption('-movflags faststart') |
285 | const _audio = await audio.get(localFfmpeg) | ||
286 | 316 | ||
287 | if (!_audio.audioStream) { | 317 | const parsedAudio = await audio.get(localCommand) |
288 | return localFfmpeg.noAudio() | ||
289 | } | ||
290 | 318 | ||
291 | // we favor VBR, if a good AAC encoder is available | 319 | if (!parsedAudio.audioStream) { |
292 | if ((await checkFFmpegEncoders()).get('libfdk_aac')) { | 320 | localCommand = localCommand.noAudio() |
293 | return localFfmpeg | 321 | } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available |
322 | localCommand = localCommand | ||
294 | .audioCodec('libfdk_aac') | 323 | .audioCodec('libfdk_aac') |
295 | .audioQuality(5) | 324 | .audioQuality(5) |
325 | } else { | ||
326 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | ||
327 | // of course this is far from perfect, but it might save some space in the end | ||
328 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | ||
329 | let bitrate: number | ||
330 | if (audio.bitrate[ audioCodecName ]) { | ||
331 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
332 | |||
333 | if (bitrate === -1) localCommand = localCommand.audioCodec('copy') | ||
334 | else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate) | ||
335 | } | ||
296 | } | 336 | } |
297 | 337 | ||
298 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 338 | // Constrained Encoding (VBV) |
299 | // of course this is far from perfect, but it might save some space in the end | 339 | // https://slhck.info/video/2017/03/01/rate-control.html |
300 | const audioCodecName = _audio.audioStream['codec_name'] | 340 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
301 | let bitrate: number | 341 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
302 | if (audio.bitrate[audioCodecName]) { | 342 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) |
303 | bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate']) | ||
304 | |||
305 | if (bitrate === -1) return localFfmpeg.audioCodec('copy') | ||
306 | } | ||
307 | 343 | ||
308 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) | 344 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
345 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
346 | // https://superuser.com/a/908325 | ||
347 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | ||
309 | 348 | ||
310 | return localFfmpeg | 349 | return localCommand |
311 | } | 350 | } |
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index cb5f27240..8ef7b1359 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -62,10 +62,7 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) | |||
62 | 62 | ||
63 | return jsig.promises | 63 | return jsig.promises |
64 | .verify(signedDocument, options) | 64 | .verify(signedDocument, options) |
65 | .then((result: { verified: boolean }) => { | 65 | .then((result: { verified: boolean }) => result.verified) |
66 | logger.info('coucou', result) | ||
67 | return result.verified | ||
68 | }) | ||
69 | .catch(err => { | 66 | .catch(err => { |
70 | logger.error('Cannot check signature.', { err }) | 67 | logger.error('Cannot check signature.', { err }) |
71 | return false | 68 | return false |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 6228fec04..049c3f8bc 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () { | |||
40 | const application = await ApplicationModel.load() | 40 | const application = await ApplicationModel.load() |
41 | if (!application) throw Error('Could not load Application from database.') | 41 | if (!application) throw Error('Could not load Application from database.') |
42 | 42 | ||
43 | return application.Account.Actor | 43 | const actor = application.Account.Actor |
44 | actor.Account = application.Account | ||
45 | |||
46 | return actor | ||
44 | }) | 47 | }) |
45 | 48 | ||
46 | function generateVideoTmpPath (target: string | ParseTorrent) { | 49 | function generateVideoTmpPath (target: string | ParseTorrent) { |
@@ -77,6 +80,20 @@ async function getVersion () { | |||
77 | return require('../../../package.json').version | 80 | return require('../../../package.json').version |
78 | } | 81 | } |
79 | 82 | ||
83 | /** | ||
84 | * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns | ||
85 | * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does | ||
86 | * not contain a UUID, returns null. | ||
87 | */ | ||
88 | function getUUIDFromFilename (filename: string) { | ||
89 | const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ | ||
90 | const result = filename.match(regex) | ||
91 | |||
92 | if (!result || Array.isArray(result) === false) return null | ||
93 | |||
94 | return result[0] | ||
95 | } | ||
96 | |||
80 | // --------------------------------------------------------------------------- | 97 | // --------------------------------------------------------------------------- |
81 | 98 | ||
82 | export { | 99 | export { |
@@ -86,5 +103,6 @@ export { | |||
86 | getSecureTorrentName, | 103 | getSecureTorrentName, |
87 | getServerActor, | 104 | getServerActor, |
88 | getVersion, | 105 | getVersion, |
89 | generateVideoTmpPath | 106 | generateVideoTmpPath, |
107 | getUUIDFromFilename | ||
90 | } | 108 | } |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index b1577a6b0..1bd21467d 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video' | |||
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 8 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 9 | ||
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 4f46d406a..9dfb5d68c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -77,7 +77,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
77 | } | 77 | } |
78 | } | 78 | } |
79 | 79 | ||
80 | checkFFmpegEncoders() | 80 | return checkFFmpegEncoders() |
81 | } | 81 | } |
82 | 82 | ||
83 | // Optional encoders, if present, can be used to improve transcoding | 83 | // Optional encoders, if present, can be used to improve transcoding |
@@ -95,10 +95,10 @@ async function checkFFmpegEncoders (): Promise<Map<string, boolean>> { | |||
95 | supportedOptionalEncoders = new Map<string, boolean>() | 95 | supportedOptionalEncoders = new Map<string, boolean>() |
96 | 96 | ||
97 | for (const encoder of optionalEncoders) { | 97 | for (const encoder of optionalEncoders) { |
98 | supportedOptionalEncoders.set(encoder, | 98 | supportedOptionalEncoders.set(encoder, encoders[encoder] !== undefined) |
99 | encoders[encoder] !== undefined | ||
100 | ) | ||
101 | } | 99 | } |
100 | |||
101 | return supportedOptionalEncoders | ||
102 | } | 102 | } |
103 | 103 | ||
104 | // --------------------------------------------------------------------------- | 104 | // --------------------------------------------------------------------------- |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index fd2308eb6..28d51068b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -3,9 +3,9 @@ import { dirname, join } from 'path' | |||
3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' |
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' |
7 | // Do not use barrels, remain constants as independent as possible | 7 | // Do not use barrels, remain constants as independent as possible |
8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, parseDuration, parseBytes, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
10 | import { invert } from 'lodash' | 10 | import { invert } from 'lodash' |
11 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' | 11 | import { CronRepeatOptions, EveryRepeatOptions } from 'bull' |
@@ -16,7 +16,7 @@ let config: IConfig = require('config') | |||
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | const LAST_MIGRATION_VERSION = 275 | 19 | const LAST_MIGRATION_VERSION = 285 |
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
@@ -47,7 +47,10 @@ const SORTABLE_COLUMNS = { | |||
47 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], | 47 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], |
48 | 48 | ||
49 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], | 49 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], |
50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] | 50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
51 | |||
52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], | ||
53 | SERVERS_BLOCKLIST: [ 'createdAt' ] | ||
51 | } | 54 | } |
52 | 55 | ||
53 | const OAUTH_LIFETIME = { | 56 | const OAUTH_LIFETIME = { |
@@ -232,8 +235,8 @@ const CONFIG = { | |||
232 | } | 235 | } |
233 | }, | 236 | }, |
234 | USER: { | 237 | USER: { |
235 | get VIDEO_QUOTA () { return config.get<number>('user.video_quota') }, | 238 | get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) }, |
236 | get VIDEO_QUOTA_DAILY () { return config.get<number>('user.video_quota_daily') } | 239 | get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) } |
237 | }, | 240 | }, |
238 | TRANSCODING: { | 241 | TRANSCODING: { |
239 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, | 242 | get ENABLED () { return config.get<boolean>('transcoding.enabled') }, |
@@ -292,7 +295,7 @@ const CONFIG = { | |||
292 | const CONSTRAINTS_FIELDS = { | 295 | const CONSTRAINTS_FIELDS = { |
293 | USERS: { | 296 | USERS: { |
294 | NAME: { min: 3, max: 120 }, // Length | 297 | NAME: { min: 3, max: 120 }, // Length |
295 | DESCRIPTION: { min: 3, max: 250 }, // Length | 298 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
296 | USERNAME: { min: 3, max: 20 }, // Length | 299 | USERNAME: { min: 3, max: 20 }, // Length |
297 | PASSWORD: { min: 6, max: 255 }, // Length | 300 | PASSWORD: { min: 6, max: 255 }, // Length |
298 | VIDEO_QUOTA: { min: -1 }, | 301 | VIDEO_QUOTA: { min: -1 }, |
@@ -308,8 +311,8 @@ const CONSTRAINTS_FIELDS = { | |||
308 | }, | 311 | }, |
309 | VIDEO_CHANNELS: { | 312 | VIDEO_CHANNELS: { |
310 | NAME: { min: 3, max: 120 }, // Length | 313 | NAME: { min: 3, max: 120 }, // Length |
311 | DESCRIPTION: { min: 3, max: 500 }, // Length | 314 | DESCRIPTION: { min: 3, max: 1000 }, // Length |
312 | SUPPORT: { min: 3, max: 500 }, // Length | 315 | SUPPORT: { min: 3, max: 1000 }, // Length |
313 | URL: { min: 3, max: 2000 } // Length | 316 | URL: { min: 3, max: 2000 } // Length |
314 | }, | 317 | }, |
315 | VIDEO_CAPTIONS: { | 318 | VIDEO_CAPTIONS: { |
@@ -338,7 +341,7 @@ const CONSTRAINTS_FIELDS = { | |||
338 | LANGUAGE: { min: 1, max: 10 }, // Length | 341 | LANGUAGE: { min: 1, max: 10 }, // Length |
339 | TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length | 342 | TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length |
340 | DESCRIPTION: { min: 3, max: 10000 }, // Length | 343 | DESCRIPTION: { min: 3, max: 10000 }, // Length |
341 | SUPPORT: { min: 3, max: 500 }, // Length | 344 | SUPPORT: { min: 3, max: 1000 }, // Length |
342 | IMAGE: { | 345 | IMAGE: { |
343 | EXTNAME: [ '.jpg', '.jpeg' ], | 346 | EXTNAME: [ '.jpg', '.jpeg' ], |
344 | FILE_SIZE: { | 347 | FILE_SIZE: { |
@@ -393,7 +396,7 @@ const RATES_LIMIT = { | |||
393 | } | 396 | } |
394 | 397 | ||
395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 398 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
396 | const VIDEO_TRANSCODING_FPS = { | 399 | const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { |
397 | MIN: 10, | 400 | MIN: 10, |
398 | AVERAGE: 30, | 401 | AVERAGE: 30, |
399 | MAX: 60, | 402 | MAX: 60, |
@@ -421,7 +424,7 @@ const VIDEO_CATEGORIES = { | |||
421 | 8: 'People', | 424 | 8: 'People', |
422 | 9: 'Comedy', | 425 | 9: 'Comedy', |
423 | 10: 'Entertainment', | 426 | 10: 'Entertainment', |
424 | 11: 'News', | 427 | 11: 'News & Politics', |
425 | 12: 'How To', | 428 | 12: 'How To', |
426 | 13: 'Education', | 429 | 13: 'Education', |
427 | 14: 'Activism', | 430 | 14: 'Activism', |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 4d57bf8aa..dd5b9bf67 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -28,6 +28,9 @@ import { VideoImportModel } from '../models/video/video-import' | |||
28 | import { VideoViewModel } from '../models/video/video-views' | 28 | import { VideoViewModel } from '../models/video/video-views' |
29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | 29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' |
30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | ||
32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
31 | 34 | ||
32 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 35 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
33 | 36 | ||
@@ -89,7 +92,10 @@ async function initDatabaseModels (silent: boolean) { | |||
89 | ScheduleVideoUpdateModel, | 92 | ScheduleVideoUpdateModel, |
90 | VideoImportModel, | 93 | VideoImportModel, |
91 | VideoViewModel, | 94 | VideoViewModel, |
92 | VideoRedundancyModel | 95 | VideoRedundancyModel, |
96 | UserVideoHistoryModel, | ||
97 | AccountBlocklistModel, | ||
98 | ServerBlocklistModel | ||
93 | ]) | 99 | ]) |
94 | 100 | ||
95 | // Check extensions exist in the database | 101 | // Check extensions exist in the database |
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts index 63f3984dd..6d253f04f 100644 --- a/server/initializers/migrations/0120-video-null.ts +++ b/server/initializers/migrations/0120-video-null.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction, | 4 | transaction: Sequelize.Transaction, |
@@ -28,7 +27,7 @@ async function up (utils: { | |||
28 | 27 | ||
29 | { | 28 | { |
30 | const data = { | 29 | const data = { |
31 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | 30 | type: Sequelize.STRING(10000), |
32 | allowNull: true, | 31 | allowNull: true, |
33 | defaultValue: null | 32 | defaultValue: null |
34 | } | 33 | } |
diff --git a/server/initializers/migrations/0195-support.ts b/server/initializers/migrations/0195-support.ts index 8722a5f22..3b9eabe79 100644 --- a/server/initializers/migrations/0195-support.ts +++ b/server/initializers/migrations/0195-support.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../index' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction, | 4 | transaction: Sequelize.Transaction, |
@@ -8,7 +7,7 @@ async function up (utils: { | |||
8 | }): Promise<void> { | 7 | }): Promise<void> { |
9 | { | 8 | { |
10 | const data = { | 9 | const data = { |
11 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max), | 10 | type: Sequelize.STRING(500), |
12 | allowNull: true, | 11 | allowNull: true, |
13 | defaultValue: null | 12 | defaultValue: null |
14 | } | 13 | } |
@@ -17,7 +16,7 @@ async function up (utils: { | |||
17 | 16 | ||
18 | { | 17 | { |
19 | const data = { | 18 | const data = { |
20 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max), | 19 | type: Sequelize.STRING(500), |
21 | allowNull: true, | 20 | allowNull: true, |
22 | defaultValue: null | 21 | defaultValue: null |
23 | } | 22 | } |
@@ -26,7 +25,7 @@ async function up (utils: { | |||
26 | 25 | ||
27 | { | 26 | { |
28 | const data = { | 27 | const data = { |
29 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max), | 28 | type: Sequelize.STRING(250), |
30 | allowNull: true, | 29 | allowNull: true, |
31 | defaultValue: null | 30 | defaultValue: null |
32 | } | 31 | } |
@@ -35,7 +34,7 @@ async function up (utils: { | |||
35 | 34 | ||
36 | { | 35 | { |
37 | const data = { | 36 | const data = { |
38 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), | 37 | type: Sequelize.STRING(10000), |
39 | allowNull: true, | 38 | allowNull: true, |
40 | defaultValue: null | 39 | defaultValue: null |
41 | } | 40 | } |
diff --git a/server/initializers/migrations/0245-user-blocked.ts b/server/initializers/migrations/0245-user-blocked.ts index 5a04ecd2b..19c7d5b9c 100644 --- a/server/initializers/migrations/0245-user-blocked.ts +++ b/server/initializers/migrations/0245-user-blocked.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 4 | transaction: Sequelize.Transaction |
@@ -31,7 +30,7 @@ async function up (utils: { | |||
31 | 30 | ||
32 | { | 31 | { |
33 | const data = { | 32 | const data = { |
34 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max), | 33 | type: Sequelize.STRING(250), |
35 | allowNull: true, | 34 | allowNull: true, |
36 | defaultValue: null | 35 | defaultValue: null |
37 | } | 36 | } |
diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts index acb668ae1..50de25182 100644 --- a/server/initializers/migrations/0250-video-abuse-state.ts +++ b/server/initializers/migrations/0250-video-abuse-state.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | import { VideoAbuseState } from '../../../shared/models/videos' | 2 | import { VideoAbuseState } from '../../../shared/models/videos' |
4 | 3 | ||
5 | async function up (utils: { | 4 | async function up (utils: { |
@@ -32,7 +31,7 @@ async function up (utils: { | |||
32 | 31 | ||
33 | { | 32 | { |
34 | const data = { | 33 | const data = { |
35 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max), | 34 | type: Sequelize.STRING(300), |
36 | allowNull: true, | 35 | allowNull: true, |
37 | defaultValue: null | 36 | defaultValue: null |
38 | } | 37 | } |
diff --git a/server/initializers/migrations/0255-video-blacklist-reason.ts b/server/initializers/migrations/0255-video-blacklist-reason.ts index a380e620e..69d6efb9e 100644 --- a/server/initializers/migrations/0255-video-blacklist-reason.ts +++ b/server/initializers/migrations/0255-video-blacklist-reason.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | import { VideoAbuseState } from '../../../shared/models/videos' | 2 | import { VideoAbuseState } from '../../../shared/models/videos' |
4 | 3 | ||
5 | async function up (utils: { | 4 | async function up (utils: { |
@@ -10,7 +9,7 @@ async function up (utils: { | |||
10 | 9 | ||
11 | { | 10 | { |
12 | const data = { | 11 | const data = { |
13 | type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max), | 12 | type: Sequelize.STRING(300), |
14 | allowNull: true, | 13 | allowNull: true, |
15 | defaultValue: null | 14 | defaultValue: null |
16 | } | 15 | } |
diff --git a/server/initializers/migrations/0260-upload-quota-daily.ts b/server/initializers/migrations/0260-upload-quota-daily.ts index d25154ba6..cbbe391ef 100644 --- a/server/initializers/migrations/0260-upload-quota-daily.ts +++ b/server/initializers/migrations/0260-upload-quota-daily.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | 2 | ||
4 | async function up (utils: { | 3 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 4 | transaction: Sequelize.Transaction |
diff --git a/server/initializers/migrations/0280-webtorrent-policy-user.ts b/server/initializers/migrations/0280-webtorrent-policy-user.ts new file mode 100644 index 000000000..e6488356a --- /dev/null +++ b/server/initializers/migrations/0280-webtorrent-policy-user.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<any> { | ||
8 | { | ||
9 | const data = { | ||
10 | type: Sequelize.BOOLEAN, | ||
11 | allowNull: false, | ||
12 | defaultValue: true | ||
13 | } | ||
14 | |||
15 | await utils.queryInterface.addColumn('user', 'webTorrentEnabled', data) | ||
16 | } | ||
17 | |||
18 | } | ||
19 | |||
20 | async function down (utils: { | ||
21 | transaction: Sequelize.Transaction | ||
22 | queryInterface: Sequelize.QueryInterface | ||
23 | sequelize: Sequelize.Sequelize | ||
24 | }): Promise<any> { | ||
25 | await utils.queryInterface.removeColumn('user', 'webTorrentEnabled') | ||
26 | } | ||
27 | |||
28 | export { up, down } | ||
diff --git a/server/initializers/migrations/0285-description-support.ts b/server/initializers/migrations/0285-description-support.ts new file mode 100644 index 000000000..85ef4ef39 --- /dev/null +++ b/server/initializers/migrations/0285-description-support.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.STRING(1000), | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | await utils.queryInterface.changeColumn('video', 'support', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const data = { | ||
20 | type: Sequelize.STRING(1000), | ||
21 | allowNull: true, | ||
22 | defaultValue: null | ||
23 | } | ||
24 | await utils.queryInterface.changeColumn('videoChannel', 'support', data) | ||
25 | } | ||
26 | |||
27 | { | ||
28 | const data = { | ||
29 | type: Sequelize.STRING(1000), | ||
30 | allowNull: true, | ||
31 | defaultValue: null | ||
32 | } | ||
33 | await utils.queryInterface.changeColumn('videoChannel', 'description', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const data = { | ||
38 | type: Sequelize.STRING(1000), | ||
39 | allowNull: true, | ||
40 | defaultValue: null | ||
41 | } | ||
42 | await utils.queryInterface.changeColumn('account', 'description', data) | ||
43 | } | ||
44 | } | ||
45 | |||
46 | function down (options) { | ||
47 | throw new Error('Not implemented.') | ||
48 | } | ||
49 | |||
50 | export { | ||
51 | up, | ||
52 | down | ||
53 | } | ||
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 55912341c..db9ce3293 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' | 1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' |
2 | import { doRequest } from '../../helpers/requests' | 2 | import { doRequest } from '../../helpers/requests' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import Bluebird = require('bluebird') | 4 | import * as Bluebird from 'bluebird' |
5 | 5 | ||
6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { |
7 | logger.info('Crawling ActivityPub data on %s.', uri) | 7 | logger.info('Crawling ActivityPub data on %s.', uri) |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 54cea542f..3da363c0a 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -310,7 +310,8 @@ export { | |||
310 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 310 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
311 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | 311 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) |
312 | 312 | ||
313 | return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') | 313 | const urlMediaType = url.mediaType || url.mimeType |
314 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | ||
314 | } | 315 | } |
315 | 316 | ||
316 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 317 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
@@ -468,7 +469,8 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
468 | for (const fileUrl of fileUrls) { | 469 | for (const fileUrl of fileUrls) { |
469 | // Fetch associated magnet uri | 470 | // Fetch associated magnet uri |
470 | const magnet = videoObject.url.find(u => { | 471 | const magnet = videoObject.url.find(u => { |
471 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height | 472 | const mediaType = u.mediaType || u.mimeType |
473 | return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height | ||
472 | }) | 474 | }) |
473 | 475 | ||
474 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | 476 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
@@ -478,8 +480,9 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
478 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 480 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
479 | } | 481 | } |
480 | 482 | ||
483 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | ||
481 | const attribute = { | 484 | const attribute = { |
482 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | 485 | extname: VIDEO_MIMETYPE_EXT[ mediaType ], |
483 | infoHash: parsed.infoHash, | 486 | infoHash: parsed.infoHash, |
484 | resolution: fileUrl.height, | 487 | resolution: fileUrl.height, |
485 | size: fileUrl.size, | 488 | size: fileUrl.size, |
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts new file mode 100644 index 000000000..1633e500c --- /dev/null +++ b/server/lib/blocklist.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { sequelizeTypescript } from '../initializers' | ||
2 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
3 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | ||
4 | |||
5 | function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { | ||
6 | return sequelizeTypescript.transaction(async t => { | ||
7 | return AccountBlocklistModel.upsert({ | ||
8 | accountId: byAccountId, | ||
9 | targetAccountId: targetAccountId | ||
10 | }, { transaction: t }) | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | function addServerInBlocklist (byAccountId: number, targetServerId: number) { | ||
15 | return sequelizeTypescript.transaction(async t => { | ||
16 | return ServerBlocklistModel.upsert({ | ||
17 | accountId: byAccountId, | ||
18 | targetServerId | ||
19 | }, { transaction: t }) | ||
20 | }) | ||
21 | } | ||
22 | |||
23 | function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) { | ||
24 | return sequelizeTypescript.transaction(async t => { | ||
25 | return accountBlock.destroy({ transaction: t }) | ||
26 | }) | ||
27 | } | ||
28 | |||
29 | function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) { | ||
30 | return sequelizeTypescript.transaction(async t => { | ||
31 | return serverBlock.destroy({ transaction: t }) | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | export { | ||
36 | addAccountInBlocklist, | ||
37 | addServerInBlocklist, | ||
38 | removeAccountFromBlocklist, | ||
39 | removeServerFromBlocklist | ||
40 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index fc013e0c3..006b25bfd 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -116,7 +116,7 @@ export class ClientHtml { | |||
116 | 116 | ||
117 | 'og:video:url': embedUrl, | 117 | 'og:video:url': embedUrl, |
118 | 'og:video:secure_url': embedUrl, | 118 | 'og:video:secure_url': embedUrl, |
119 | 'og:video:type': 'text/html', | 119 | 'og:video:type': 'video/mp4', |
120 | 'og:video:width': EMBED_SIZE.width, | 120 | 'og:video:width': EMBED_SIZE.width, |
121 | 'og:video:height': EMBED_SIZE.height, | 121 | 'og:video:height': EMBED_SIZE.height, |
122 | 122 | ||
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 1463c93fc..adc0a2a15 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -8,7 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' | 11 | import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' |
12 | 12 | ||
13 | export type VideoFilePayload = { | 13 | export type VideoFilePayload = { |
14 | videoUUID: string | 14 | videoUUID: string |
@@ -56,7 +56,7 @@ async function processVideoFile (job: Bull.Job) { | |||
56 | 56 | ||
57 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) | 57 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) |
58 | } else { | 58 | } else { |
59 | await optimizeOriginalVideofile(video) | 59 | await optimizeVideofile(video) |
60 | 60 | ||
61 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) | 61 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) |
62 | } | 62 | } |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index e4e435659..abd75d512 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -48,6 +48,8 @@ class Redis { | |||
48 | ) | 48 | ) |
49 | } | 49 | } |
50 | 50 | ||
51 | /************* Forgot password *************/ | ||
52 | |||
51 | async setResetPasswordVerificationString (userId: number) { | 53 | async setResetPasswordVerificationString (userId: number) { |
52 | const generatedString = await generateRandomString(32) | 54 | const generatedString = await generateRandomString(32) |
53 | 55 | ||
@@ -60,6 +62,8 @@ class Redis { | |||
60 | return this.getValue(this.generateResetPasswordKey(userId)) | 62 | return this.getValue(this.generateResetPasswordKey(userId)) |
61 | } | 63 | } |
62 | 64 | ||
65 | /************* Email verification *************/ | ||
66 | |||
63 | async setVerifyEmailVerificationString (userId: number) { | 67 | async setVerifyEmailVerificationString (userId: number) { |
64 | const generatedString = await generateRandomString(32) | 68 | const generatedString = await generateRandomString(32) |
65 | 69 | ||
@@ -72,16 +76,20 @@ class Redis { | |||
72 | return this.getValue(this.generateVerifyEmailKey(userId)) | 76 | return this.getValue(this.generateVerifyEmailKey(userId)) |
73 | } | 77 | } |
74 | 78 | ||
79 | /************* Views per IP *************/ | ||
80 | |||
75 | setIPVideoView (ip: string, videoUUID: string) { | 81 | setIPVideoView (ip: string, videoUUID: string) { |
76 | return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) | 82 | return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) |
77 | } | 83 | } |
78 | 84 | ||
79 | async isVideoIPViewExists (ip: string, videoUUID: string) { | 85 | async isVideoIPViewExists (ip: string, videoUUID: string) { |
80 | return this.exists(this.buildViewKey(ip, videoUUID)) | 86 | return this.exists(this.generateViewKey(ip, videoUUID)) |
81 | } | 87 | } |
82 | 88 | ||
89 | /************* API cache *************/ | ||
90 | |||
83 | async getCachedRoute (req: express.Request) { | 91 | async getCachedRoute (req: express.Request) { |
84 | const cached = await this.getObject(this.buildCachedRouteKey(req)) | 92 | const cached = await this.getObject(this.generateCachedRouteKey(req)) |
85 | 93 | ||
86 | return cached as CachedRoute | 94 | return cached as CachedRoute |
87 | } | 95 | } |
@@ -94,9 +102,11 @@ class Redis { | |||
94 | (statusCode) ? { statusCode: statusCode.toString() } : null | 102 | (statusCode) ? { statusCode: statusCode.toString() } : null |
95 | ) | 103 | ) |
96 | 104 | ||
97 | return this.setObject(this.buildCachedRouteKey(req), cached, lifetime) | 105 | return this.setObject(this.generateCachedRouteKey(req), cached, lifetime) |
98 | } | 106 | } |
99 | 107 | ||
108 | /************* Video views *************/ | ||
109 | |||
100 | addVideoView (videoId: number) { | 110 | addVideoView (videoId: number) { |
101 | const keyIncr = this.generateVideoViewKey(videoId) | 111 | const keyIncr = this.generateVideoViewKey(videoId) |
102 | const keySet = this.generateVideosViewKey() | 112 | const keySet = this.generateVideosViewKey() |
@@ -131,33 +141,37 @@ class Redis { | |||
131 | ]) | 141 | ]) |
132 | } | 142 | } |
133 | 143 | ||
134 | generateVideosViewKey (hour?: number) { | 144 | /************* Keys generation *************/ |
145 | |||
146 | generateCachedRouteKey (req: express.Request) { | ||
147 | return req.method + '-' + req.originalUrl | ||
148 | } | ||
149 | |||
150 | private generateVideosViewKey (hour?: number) { | ||
135 | if (!hour) hour = new Date().getHours() | 151 | if (!hour) hour = new Date().getHours() |
136 | 152 | ||
137 | return `videos-view-h${hour}` | 153 | return `videos-view-h${hour}` |
138 | } | 154 | } |
139 | 155 | ||
140 | generateVideoViewKey (videoId: number, hour?: number) { | 156 | private generateVideoViewKey (videoId: number, hour?: number) { |
141 | if (!hour) hour = new Date().getHours() | 157 | if (!hour) hour = new Date().getHours() |
142 | 158 | ||
143 | return `video-view-${videoId}-h${hour}` | 159 | return `video-view-${videoId}-h${hour}` |
144 | } | 160 | } |
145 | 161 | ||
146 | generateResetPasswordKey (userId: number) { | 162 | private generateResetPasswordKey (userId: number) { |
147 | return 'reset-password-' + userId | 163 | return 'reset-password-' + userId |
148 | } | 164 | } |
149 | 165 | ||
150 | generateVerifyEmailKey (userId: number) { | 166 | private generateVerifyEmailKey (userId: number) { |
151 | return 'verify-email-' + userId | 167 | return 'verify-email-' + userId |
152 | } | 168 | } |
153 | 169 | ||
154 | buildViewKey (ip: string, videoUUID: string) { | 170 | private generateViewKey (ip: string, videoUUID: string) { |
155 | return videoUUID + '-' + ip | 171 | return videoUUID + '-' + ip |
156 | } | 172 | } |
157 | 173 | ||
158 | buildCachedRouteKey (req: express.Request) { | 174 | /************* Redis helpers *************/ |
159 | return req.method + '-' + req.originalUrl | ||
160 | } | ||
161 | 175 | ||
162 | private getValue (key: string) { | 176 | private getValue (key: string) { |
163 | return new Promise<string>((res, rej) => { | 177 | return new Promise<string>((res, rej) => { |
@@ -197,6 +211,12 @@ class Redis { | |||
197 | }) | 211 | }) |
198 | } | 212 | } |
199 | 213 | ||
214 | private deleteFieldInHash (key: string, field: string) { | ||
215 | return new Promise<void>((res, rej) => { | ||
216 | this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res()) | ||
217 | }) | ||
218 | } | ||
219 | |||
200 | private setValue (key: string, value: string, expirationMilliseconds: number) { | 220 | private setValue (key: string, value: string, expirationMilliseconds: number) { |
201 | return new Promise<void>((res, rej) => { | 221 | return new Promise<void>((res, rej) => { |
202 | this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { | 222 | this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { |
@@ -235,6 +255,16 @@ class Redis { | |||
235 | }) | 255 | }) |
236 | } | 256 | } |
237 | 257 | ||
258 | private setValueInHash (key: string, field: string, value: string) { | ||
259 | return new Promise<void>((res, rej) => { | ||
260 | this.client.hset(this.prefix + key, field, value, (err) => { | ||
261 | if (err) return rej(err) | ||
262 | |||
263 | return res() | ||
264 | }) | ||
265 | }) | ||
266 | } | ||
267 | |||
238 | private increment (key: string) { | 268 | private increment (key: string) { |
239 | return new Promise<number>((res, rej) => { | 269 | return new Promise<number>((res, rej) => { |
240 | this.client.incr(this.prefix + key, (err, value) => { | 270 | this.client.incr(this.prefix + key, (err, value) => { |
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 70ba7c303..59bce7520 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): | |||
64 | } | 64 | } |
65 | 65 | ||
66 | const parentCommentThread = idx[childComment.inReplyToCommentId] | 66 | const parentCommentThread = idx[childComment.inReplyToCommentId] |
67 | if (!parentCommentThread) { | 67 | // Maybe the parent comment was blocked by the admin/user |
68 | const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}` | 68 | if (!parentCommentThread) continue |
69 | throw new Error(msg) | ||
70 | } | ||
71 | 69 | ||
72 | parentCommentThread.children.push(childCommentThread) | 70 | parentCommentThread.children.push(childCommentThread) |
73 | idx[childComment.id] = childCommentThread | 71 | idx[childComment.id] = childCommentThread |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index bf3ff78c2..a78de61e5 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG } from '../initializers' |
2 | import { join, extname } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, rename, stat } from 'fs-extra' | 4 | import { copy, remove, rename, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
@@ -7,10 +7,11 @@ import { VideoResolution } from '../../shared/models/videos' | |||
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | 9 | ||
10 | async function optimizeOriginalVideofile (video: VideoModel) { | 10 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { |
11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
12 | const newExtname = '.mp4' | 12 | const newExtname = '.mp4' |
13 | const inputVideoFile = video.getOriginalFile() | 13 | |
14 | const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile() | ||
14 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) | 15 | const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) |
15 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) | 16 | const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) |
16 | 17 | ||
@@ -124,7 +125,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) { | |||
124 | } | 125 | } |
125 | 126 | ||
126 | export { | 127 | export { |
127 | optimizeOriginalVideofile, | 128 | optimizeVideofile, |
128 | transcodeOriginalVideofile, | 129 | transcodeOriginalVideofile, |
129 | importVideoFile | 130 | importVideoFile |
130 | } | 131 | } |
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts index 1b44957d3..1e00fc731 100644 --- a/server/middlewares/cache.ts +++ b/server/middlewares/cache.ts | |||
@@ -8,7 +8,7 @@ const lock = new AsyncLock({ timeout: 5000 }) | |||
8 | 8 | ||
9 | function cacheRoute (lifetimeArg: string | number) { | 9 | function cacheRoute (lifetimeArg: string | number) { |
10 | return async function (req: express.Request, res: express.Response, next: express.NextFunction) { | 10 | return async function (req: express.Request, res: express.Response, next: express.NextFunction) { |
11 | const redisKey = Redis.Instance.buildCachedRouteKey(req) | 11 | const redisKey = Redis.Instance.generateCachedRouteKey(req) |
12 | 12 | ||
13 | try { | 13 | try { |
14 | await lock.acquire(redisKey, async (done) => { | 14 | await lock.acquire(redisKey, async (done) => { |
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts new file mode 100644 index 000000000..109276c63 --- /dev/null +++ b/server/middlewares/validators/blocklist.ts | |||
@@ -0,0 +1,172 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | ||
6 | import { UserModel } from '../../models/account/user' | ||
7 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' | ||
8 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
9 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' | ||
10 | import { ServerModel } from '../../models/server/server' | ||
11 | import { CONFIG } from '../../initializers' | ||
12 | import { getServerActor } from '../../helpers/utils' | ||
13 | |||
14 | const blockAccountValidator = [ | ||
15 | body('accountName').exists().withMessage('Should have an account name with host'), | ||
16 | |||
17 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
18 | logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body }) | ||
19 | |||
20 | if (areValidationErrors(req, res)) return | ||
21 | if (!await isAccountNameWithHostExist(req.body.accountName, res)) return | ||
22 | |||
23 | const user = res.locals.oauth.token.User as UserModel | ||
24 | const accountToBlock = res.locals.account | ||
25 | |||
26 | if (user.Account.id === accountToBlock.id) { | ||
27 | res.status(409) | ||
28 | .send({ error: 'You cannot block yourself.' }) | ||
29 | .end() | ||
30 | |||
31 | return | ||
32 | } | ||
33 | |||
34 | return next() | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | const unblockAccountByAccountValidator = [ | ||
39 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
40 | |||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params }) | ||
43 | |||
44 | if (areValidationErrors(req, res)) return | ||
45 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
46 | |||
47 | const user = res.locals.oauth.token.User as UserModel | ||
48 | const targetAccount = res.locals.account | ||
49 | if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return | ||
50 | |||
51 | return next() | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | const unblockAccountByServerValidator = [ | ||
56 | param('accountName').exists().withMessage('Should have an account name with host'), | ||
57 | |||
58 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
59 | logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params }) | ||
60 | |||
61 | if (areValidationErrors(req, res)) return | ||
62 | if (!await isAccountNameWithHostExist(req.params.accountName, res)) return | ||
63 | |||
64 | const serverActor = await getServerActor() | ||
65 | const targetAccount = res.locals.account | ||
66 | if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | const blockServerValidator = [ | ||
73 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
74 | |||
75 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
76 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
77 | |||
78 | if (areValidationErrors(req, res)) return | ||
79 | |||
80 | const host: string = req.body.host | ||
81 | |||
82 | if (host === CONFIG.WEBSERVER.HOST) { | ||
83 | return res.status(409) | ||
84 | .send({ error: 'You cannot block your own server.' }) | ||
85 | .end() | ||
86 | } | ||
87 | |||
88 | const server = await ServerModel.loadByHost(host) | ||
89 | if (!server) { | ||
90 | return res.status(404) | ||
91 | .send({ error: 'Server host not found.' }) | ||
92 | .end() | ||
93 | } | ||
94 | |||
95 | res.locals.server = server | ||
96 | |||
97 | return next() | ||
98 | } | ||
99 | ] | ||
100 | |||
101 | const unblockServerByAccountValidator = [ | ||
102 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
103 | |||
104 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
105 | logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params }) | ||
106 | |||
107 | if (areValidationErrors(req, res)) return | ||
108 | |||
109 | const user = res.locals.oauth.token.User as UserModel | ||
110 | if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return | ||
111 | |||
112 | return next() | ||
113 | } | ||
114 | ] | ||
115 | |||
116 | const unblockServerByServerValidator = [ | ||
117 | param('host').custom(isHostValid).withMessage('Should have an account name with host'), | ||
118 | |||
119 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
120 | logger.debug('Checking unblockServerByServerValidator parameters', { parameters: req.params }) | ||
121 | |||
122 | if (areValidationErrors(req, res)) return | ||
123 | |||
124 | const serverActor = await getServerActor() | ||
125 | if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return | ||
126 | |||
127 | return next() | ||
128 | } | ||
129 | ] | ||
130 | |||
131 | // --------------------------------------------------------------------------- | ||
132 | |||
133 | export { | ||
134 | blockServerValidator, | ||
135 | blockAccountValidator, | ||
136 | unblockAccountByAccountValidator, | ||
137 | unblockServerByAccountValidator, | ||
138 | unblockAccountByServerValidator, | ||
139 | unblockServerByServerValidator | ||
140 | } | ||
141 | |||
142 | // --------------------------------------------------------------------------- | ||
143 | |||
144 | async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) { | ||
145 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) | ||
146 | if (!accountBlock) { | ||
147 | res.status(404) | ||
148 | .send({ error: 'Account block entry not found.' }) | ||
149 | .end() | ||
150 | |||
151 | return false | ||
152 | } | ||
153 | |||
154 | res.locals.accountBlock = accountBlock | ||
155 | |||
156 | return true | ||
157 | } | ||
158 | |||
159 | async function isUnblockServerExists (accountId: number, host: string, res: express.Response) { | ||
160 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) | ||
161 | if (!serverBlock) { | ||
162 | res.status(404) | ||
163 | .send({ error: 'Server block entry not found.' }) | ||
164 | .end() | ||
165 | |||
166 | return false | ||
167 | } | ||
168 | |||
169 | res.locals.serverBlock = serverBlock | ||
170 | |||
171 | return true | ||
172 | } | ||
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 940547a3e..46c7f0f3a 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './blocklist' | ||
2 | export * from './oembed' | 3 | export * from './oembed' |
3 | export * from './activitypub' | 4 | export * from './activitypub' |
4 | export * from './pagination' | 5 | export * from './pagination' |
@@ -8,9 +9,6 @@ export * from './sort' | |||
8 | export * from './users' | 9 | export * from './users' |
9 | export * from './user-subscriptions' | 10 | export * from './user-subscriptions' |
10 | export * from './videos' | 11 | export * from './videos' |
11 | export * from './video-abuses' | ||
12 | export * from './video-blacklist' | ||
13 | export * from './video-channels' | ||
14 | export * from './webfinger' | 12 | export * from './webfinger' |
15 | export * from './search' | 13 | export * from './search' |
16 | export * from './video-imports' | 14 | export * from './server' |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 8baf643a5..6a95d6095 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -2,8 +2,7 @@ import * as express from 'express' | |||
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from './utils' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { query } from 'express-validator/check' | 4 | import { query } from 'express-validator/check' |
5 | import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' | 5 | import { isDateValid } from '../../helpers/custom-validators/misc' |
6 | import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' | ||
7 | 6 | ||
8 | const videosSearchValidator = [ | 7 | const videosSearchValidator = [ |
9 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 8 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), |
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [ | |||
35 | } | 34 | } |
36 | ] | 35 | ] |
37 | 36 | ||
38 | const commonVideosFiltersValidator = [ | ||
39 | query('categoryOneOf') | ||
40 | .optional() | ||
41 | .customSanitizer(toArray) | ||
42 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
43 | query('licenceOneOf') | ||
44 | .optional() | ||
45 | .customSanitizer(toArray) | ||
46 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
47 | query('languageOneOf') | ||
48 | .optional() | ||
49 | .customSanitizer(toArray) | ||
50 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
51 | query('tagsOneOf') | ||
52 | .optional() | ||
53 | .customSanitizer(toArray) | ||
54 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
55 | query('tagsAllOf') | ||
56 | .optional() | ||
57 | .customSanitizer(toArray) | ||
58 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
59 | query('nsfw') | ||
60 | .optional() | ||
61 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
62 | |||
63 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
64 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
65 | |||
66 | if (areValidationErrors(req, res)) return | ||
67 | |||
68 | return next() | ||
69 | } | ||
70 | ] | ||
71 | |||
72 | // --------------------------------------------------------------------------- | 37 | // --------------------------------------------------------------------------- |
73 | 38 | ||
74 | export { | 39 | export { |
75 | commonVideosFiltersValidator, | ||
76 | videoChannelsSearchValidator, | 40 | videoChannelsSearchValidator, |
77 | videosSearchValidator | 41 | videosSearchValidator |
78 | } | 42 | } |
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts new file mode 100644 index 000000000..a491dfeb3 --- /dev/null +++ b/server/middlewares/validators/server.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
5 | import { ServerModel } from '../../models/server/server' | ||
6 | import { body } from 'express-validator/check' | ||
7 | |||
8 | const serverGetValidator = [ | ||
9 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | ||
10 | |||
11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
12 | logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) | ||
13 | |||
14 | if (areValidationErrors(req, res)) return | ||
15 | |||
16 | const server = await ServerModel.loadByHost(req.body.host) | ||
17 | if (!server) { | ||
18 | return res.status(404) | ||
19 | .send({ error: 'Server host not found.' }) | ||
20 | .end() | ||
21 | } | ||
22 | |||
23 | res.locals.server = server | ||
24 | |||
25 | return next() | ||
26 | } | ||
27 | ] | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | serverGetValidator | ||
33 | } | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 08dcc2680..4c0577d8f 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -16,6 +16,8 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V | |||
16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) | 16 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) |
17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) | 17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) |
18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | 18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) |
19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | ||
20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | ||
19 | 21 | ||
20 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 22 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
21 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 23 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -31,6 +33,8 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) | |||
31 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) | 33 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) |
32 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | 34 | const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) |
33 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | 35 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) |
36 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) | ||
37 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) | ||
34 | 38 | ||
35 | // --------------------------------------------------------------------------- | 39 | // --------------------------------------------------------------------------- |
36 | 40 | ||
@@ -48,5 +52,7 @@ export { | |||
48 | jobsSortValidator, | 52 | jobsSortValidator, |
49 | videoCommentThreadsSortValidator, | 53 | videoCommentThreadsSortValidator, |
50 | userSubscriptionsSortValidator, | 54 | userSubscriptionsSortValidator, |
51 | videoChannelsSearchSortValidator | 55 | videoChannelsSearchSortValidator, |
56 | accountsBlocklistSortValidator, | ||
57 | serversBlocklistSortValidator | ||
52 | } | 58 | } |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts new file mode 100644 index 000000000..294783d85 --- /dev/null +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | export * from './video-abuses' | ||
2 | export * from './video-blacklist' | ||
3 | export * from './video-captions' | ||
4 | export * from './video-channels' | ||
5 | export * from './video-comments' | ||
6 | export * from './video-imports' | ||
7 | export * from './video-watch' | ||
8 | export * from './videos' | ||
diff --git a/server/middlewares/validators/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts index f15d55a75..be26ca16a 100644 --- a/server/middlewares/validators/video-abuses.ts +++ b/server/middlewares/validators/videos/video-abuses.ts | |||
@@ -1,16 +1,16 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param } from 'express-validator/check' | 3 | import { body, param } from 'express-validator/check' |
4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
5 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 5 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { areValidationErrors } from './utils' | 7 | import { areValidationErrors } from '../utils' |
8 | import { | 8 | import { |
9 | isVideoAbuseExist, | 9 | isVideoAbuseExist, |
10 | isVideoAbuseModerationCommentValid, | 10 | isVideoAbuseModerationCommentValid, |
11 | isVideoAbuseReasonValid, | 11 | isVideoAbuseReasonValid, |
12 | isVideoAbuseStateValid | 12 | isVideoAbuseStateValid |
13 | } from '../../helpers/custom-validators/video-abuses' | 13 | } from '../../../helpers/custom-validators/video-abuses' |
14 | 14 | ||
15 | const videoAbuseReportValidator = [ | 15 | const videoAbuseReportValidator = [ |
16 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 16 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 95a2b9f17..13da7acff 100644 --- a/server/middlewares/validators/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
4 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { areValidationErrors } from './utils' | 6 | import { areValidationErrors } from '../utils' |
7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' |
8 | 8 | ||
9 | const videosBlacklistRemoveValidator = [ | 9 | const videosBlacklistRemoveValidator = [ |
10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
diff --git a/server/middlewares/validators/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 51ffd7f3c..63d84fbec 100644 --- a/server/middlewares/validators/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from '../utils' |
3 | import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos' | 3 | import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos' |
4 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
5 | import { body, param } from 'express-validator/check' | 5 | import { body, param } from 'express-validator/check' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
7 | import { UserRight } from '../../../shared' | 7 | import { UserRight } from '../../../../shared' |
8 | import { logger } from '../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 9 | import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' |
10 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 10 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
11 | 11 | ||
12 | const addVideoCaptionValidator = [ | 12 | const addVideoCaptionValidator = [ |
13 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 13 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 56a347b39..f039794e0 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -1,20 +1,20 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { UserRight } from '../../../shared' | 3 | import { UserRight } from '../../../../shared' |
4 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | 4 | import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts' |
5 | import { | 5 | import { |
6 | isLocalVideoChannelNameExist, | 6 | isLocalVideoChannelNameExist, |
7 | isVideoChannelDescriptionValid, | 7 | isVideoChannelDescriptionValid, |
8 | isVideoChannelNameValid, | 8 | isVideoChannelNameValid, |
9 | isVideoChannelNameWithHostExist, | 9 | isVideoChannelNameWithHostExist, |
10 | isVideoChannelSupportValid | 10 | isVideoChannelSupportValid |
11 | } from '../../helpers/custom-validators/video-channels' | 11 | } from '../../../helpers/custom-validators/video-channels' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../../helpers/logger' |
13 | import { UserModel } from '../../models/account/user' | 13 | import { UserModel } from '../../../models/account/user' |
14 | import { VideoChannelModel } from '../../models/video/video-channel' | 14 | import { VideoChannelModel } from '../../../models/video/video-channel' |
15 | import { areValidationErrors } from './utils' | 15 | import { areValidationErrors } from '../utils' |
16 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' | 16 | import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' |
17 | import { ActorModel } from '../../models/activitypub/actor' | 17 | import { ActorModel } from '../../../models/activitypub/actor' |
18 | 18 | ||
19 | const listVideoAccountChannelsValidator = [ | 19 | const listVideoAccountChannelsValidator = [ |
20 | param('accountName').exists().withMessage('Should have a valid account name'), | 20 | param('accountName').exists().withMessage('Should have a valid account name'), |
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 693852499..348d33082 100644 --- a/server/middlewares/validators/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { UserRight } from '../../../shared' | 3 | import { UserRight } from '../../../../shared' |
4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
5 | import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' | 5 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' |
6 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 6 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { UserModel } from '../../models/account/user' | 8 | import { UserModel } from '../../../models/account/user' |
9 | import { VideoModel } from '../../models/video/video' | 9 | import { VideoModel } from '../../../models/video/video' |
10 | import { VideoCommentModel } from '../../models/video/video-comment' | 10 | import { VideoCommentModel } from '../../../models/video/video-comment' |
11 | import { areValidationErrors } from './utils' | 11 | import { areValidationErrors } from '../utils' |
12 | 12 | ||
13 | const listVideoCommentThreadsValidator = [ | 13 | const listVideoCommentThreadsValidator = [ |
14 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 14 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
diff --git a/server/middlewares/validators/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index b2063b8da..48d20f904 100644 --- a/server/middlewares/validators/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator/check' | 2 | import { body } from 'express-validator/check' |
3 | import { isIdValid } from '../../helpers/custom-validators/misc' | 3 | import { isIdValid } from '../../../helpers/custom-validators/misc' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from '../utils' |
6 | import { getCommonVideoAttributes } from './videos' | 6 | import { getCommonVideoAttributes } from './videos' |
7 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports' | 7 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
8 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 8 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
9 | import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos' | 9 | import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' |
10 | import { CONFIG } from '../../initializers/constants' | 10 | import { CONFIG } from '../../../initializers/constants' |
11 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 11 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
12 | 12 | ||
13 | const videoImportAddValidator = getCommonVideoAttributes().concat([ | 13 | const videoImportAddValidator = getCommonVideoAttributes().concat([ |
14 | body('channelId') | 14 | body('channelId') |
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts new file mode 100644 index 000000000..bca64662f --- /dev/null +++ b/server/middlewares/validators/videos/video-watch.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | ||
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | ||
5 | import { areValidationErrors } from '../utils' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | |||
8 | const videoWatchingValidator = [ | ||
9 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
10 | body('currentTime') | ||
11 | .toInt() | ||
12 | .isInt().withMessage('Should have correct current time'), | ||
13 | |||
14 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
15 | logger.debug('Checking videoWatching parameters', { parameters: req.body }) | ||
16 | |||
17 | if (areValidationErrors(req, res)) return | ||
18 | if (!await isVideoExist(req.params.videoId, res, 'id')) return | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | export { | ||
27 | videoWatchingValidator | ||
28 | } | ||
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos/videos.ts index 67eabe468..9dc52a134 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, ValidationChain } from 'express-validator/check' | 3 | import { body, param, query, ValidationChain } from 'express-validator/check' |
4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' | 4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
5 | import { | 5 | import { |
6 | isBooleanValid, | 6 | isBooleanValid, |
7 | isDateValid, | 7 | isDateValid, |
8 | isIdOrUUIDValid, | 8 | isIdOrUUIDValid, |
9 | isIdValid, | 9 | isIdValid, |
10 | isUUIDValid, | 10 | isUUIDValid, |
11 | toArray, | ||
11 | toIntOrNull, | 12 | toIntOrNull, |
12 | toValueOrNull | 13 | toValueOrNull |
13 | } from '../../helpers/custom-validators/misc' | 14 | } from '../../../helpers/custom-validators/misc' |
14 | import { | 15 | import { |
15 | checkUserCanManageVideo, | 16 | checkUserCanManageVideo, |
16 | isScheduleVideoUpdatePrivacyValid, | 17 | isScheduleVideoUpdatePrivacyValid, |
@@ -19,6 +20,7 @@ import { | |||
19 | isVideoDescriptionValid, | 20 | isVideoDescriptionValid, |
20 | isVideoExist, | 21 | isVideoExist, |
21 | isVideoFile, | 22 | isVideoFile, |
23 | isVideoFilterValid, | ||
22 | isVideoImage, | 24 | isVideoImage, |
23 | isVideoLanguageValid, | 25 | isVideoLanguageValid, |
24 | isVideoLicenceValid, | 26 | isVideoLicenceValid, |
@@ -27,21 +29,22 @@ import { | |||
27 | isVideoRatingTypeValid, | 29 | isVideoRatingTypeValid, |
28 | isVideoSupportValid, | 30 | isVideoSupportValid, |
29 | isVideoTagsValid | 31 | isVideoTagsValid |
30 | } from '../../helpers/custom-validators/videos' | 32 | } from '../../../helpers/custom-validators/videos' |
31 | import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' | 33 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' |
32 | import { logger } from '../../helpers/logger' | 34 | import { logger } from '../../../helpers/logger' |
33 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 35 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
34 | import { VideoShareModel } from '../../models/video/video-share' | 36 | import { VideoShareModel } from '../../../models/video/video-share' |
35 | import { authenticate } from '../oauth' | 37 | import { authenticate } from '../../oauth' |
36 | import { areValidationErrors } from './utils' | 38 | import { areValidationErrors } from '../utils' |
37 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 39 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
38 | import { VideoModel } from '../../models/video/video' | 40 | import { VideoModel } from '../../../models/video/video' |
39 | import { UserModel } from '../../models/account/user' | 41 | import { UserModel } from '../../../models/account/user' |
40 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' | 42 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' |
41 | import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' | 43 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' |
42 | import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' | 44 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' |
43 | import { AccountModel } from '../../models/account/account' | 45 | import { AccountModel } from '../../../models/account/account' |
44 | import { VideoFetchType } from '../../helpers/video' | 46 | import { VideoFetchType } from '../../../helpers/video' |
47 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
45 | 48 | ||
46 | const videosAddValidator = getCommonVideoAttributes().concat([ | 49 | const videosAddValidator = getCommonVideoAttributes().concat([ |
47 | body('videofile') | 50 | body('videofile') |
@@ -69,7 +72,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
69 | if (isAble === false) { | 72 | if (isAble === false) { |
70 | res.status(403) | 73 | res.status(403) |
71 | .json({ error: 'The user video quota is exceeded with this video.' }) | 74 | .json({ error: 'The user video quota is exceeded with this video.' }) |
72 | .end() | ||
73 | 75 | ||
74 | return cleanUpReqFiles(req) | 76 | return cleanUpReqFiles(req) |
75 | } | 77 | } |
@@ -82,7 +84,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([ | |||
82 | logger.error('Invalid input file in videosAddValidator.', { err }) | 84 | logger.error('Invalid input file in videosAddValidator.', { err }) |
83 | res.status(400) | 85 | res.status(400) |
84 | .json({ error: 'Invalid input file.' }) | 86 | .json({ error: 'Invalid input file.' }) |
85 | .end() | ||
86 | 87 | ||
87 | return cleanUpReqFiles(req) | 88 | return cleanUpReqFiles(req) |
88 | } | 89 | } |
@@ -120,7 +121,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([ | |||
120 | cleanUpReqFiles(req) | 121 | cleanUpReqFiles(req) |
121 | return res.status(409) | 122 | return res.status(409) |
122 | .json({ error: 'Cannot set "private" a video that was not private.' }) | 123 | .json({ error: 'Cannot set "private" a video that was not private.' }) |
123 | .end() | ||
124 | } | 124 | } |
125 | 125 | ||
126 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 126 | if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) |
@@ -150,7 +150,6 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => { | |||
150 | if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { | 150 | if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { |
151 | return res.status(403) | 151 | return res.status(403) |
152 | .json({ error: 'Cannot get this private or blacklisted video.' }) | 152 | .json({ error: 'Cannot get this private or blacklisted video.' }) |
153 | .end() | ||
154 | } | 153 | } |
155 | 154 | ||
156 | return next() | 155 | return next() |
@@ -239,8 +238,8 @@ const videosChangeOwnershipValidator = [ | |||
239 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | 238 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) |
240 | if (!nextOwner) { | 239 | if (!nextOwner) { |
241 | res.status(400) | 240 | res.status(400) |
242 | .type('json') | 241 | .json({ error: 'Changing video ownership to a remote account is not supported yet' }) |
243 | .end() | 242 | |
244 | return | 243 | return |
245 | } | 244 | } |
246 | res.locals.nextOwner = nextOwner | 245 | res.locals.nextOwner = nextOwner |
@@ -271,7 +270,7 @@ const videosTerminateChangeOwnershipValidator = [ | |||
271 | } else { | 270 | } else { |
272 | res.status(403) | 271 | res.status(403) |
273 | .json({ error: 'Ownership already accepted or refused' }) | 272 | .json({ error: 'Ownership already accepted or refused' }) |
274 | .end() | 273 | |
275 | return | 274 | return |
276 | } | 275 | } |
277 | } | 276 | } |
@@ -288,7 +287,7 @@ const videosAcceptChangeOwnershipValidator = [ | |||
288 | if (isAble === false) { | 287 | if (isAble === false) { |
289 | res.status(403) | 288 | res.status(403) |
290 | .json({ error: 'The user video quota is exceeded with this video.' }) | 289 | .json({ error: 'The user video quota is exceeded with this video.' }) |
291 | .end() | 290 | |
292 | return | 291 | return |
293 | } | 292 | } |
294 | 293 | ||
@@ -363,6 +362,51 @@ function getCommonVideoAttributes () { | |||
363 | ] as (ValidationChain | express.Handler)[] | 362 | ] as (ValidationChain | express.Handler)[] |
364 | } | 363 | } |
365 | 364 | ||
365 | const commonVideosFiltersValidator = [ | ||
366 | query('categoryOneOf') | ||
367 | .optional() | ||
368 | .customSanitizer(toArray) | ||
369 | .custom(isNumberArray).withMessage('Should have a valid one of category array'), | ||
370 | query('licenceOneOf') | ||
371 | .optional() | ||
372 | .customSanitizer(toArray) | ||
373 | .custom(isNumberArray).withMessage('Should have a valid one of licence array'), | ||
374 | query('languageOneOf') | ||
375 | .optional() | ||
376 | .customSanitizer(toArray) | ||
377 | .custom(isStringArray).withMessage('Should have a valid one of language array'), | ||
378 | query('tagsOneOf') | ||
379 | .optional() | ||
380 | .customSanitizer(toArray) | ||
381 | .custom(isStringArray).withMessage('Should have a valid one of tags array'), | ||
382 | query('tagsAllOf') | ||
383 | .optional() | ||
384 | .customSanitizer(toArray) | ||
385 | .custom(isStringArray).withMessage('Should have a valid all of tags array'), | ||
386 | query('nsfw') | ||
387 | .optional() | ||
388 | .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), | ||
389 | query('filter') | ||
390 | .optional() | ||
391 | .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), | ||
392 | |||
393 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
394 | logger.debug('Checking commons video filters query', { parameters: req.query }) | ||
395 | |||
396 | if (areValidationErrors(req, res)) return | ||
397 | |||
398 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
399 | if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { | ||
400 | res.status(401) | ||
401 | .json({ error: 'You are not allowed to see all local videos.' }) | ||
402 | |||
403 | return | ||
404 | } | ||
405 | |||
406 | return next() | ||
407 | } | ||
408 | ] | ||
409 | |||
366 | // --------------------------------------------------------------------------- | 410 | // --------------------------------------------------------------------------- |
367 | 411 | ||
368 | export { | 412 | export { |
@@ -379,7 +423,9 @@ export { | |||
379 | videosTerminateChangeOwnershipValidator, | 423 | videosTerminateChangeOwnershipValidator, |
380 | videosAcceptChangeOwnershipValidator, | 424 | videosAcceptChangeOwnershipValidator, |
381 | 425 | ||
382 | getCommonVideoAttributes | 426 | getCommonVideoAttributes, |
427 | |||
428 | commonVideosFiltersValidator | ||
383 | } | 429 | } |
384 | 430 | ||
385 | // --------------------------------------------------------------------------- | 431 | // --------------------------------------------------------------------------- |
@@ -389,7 +435,6 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) | |||
389 | if (!req.body.scheduleUpdate.updateAt) { | 435 | if (!req.body.scheduleUpdate.updateAt) { |
390 | res.status(400) | 436 | res.status(400) |
391 | .json({ error: 'Schedule update at is mandatory.' }) | 437 | .json({ error: 'Schedule update at is mandatory.' }) |
392 | .end() | ||
393 | 438 | ||
394 | return true | 439 | return true |
395 | } | 440 | } |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts new file mode 100644 index 000000000..fa2819235 --- /dev/null +++ b/server/models/account/account-blocklist.ts | |||
@@ -0,0 +1,111 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from './account' | ||
3 | import { getSort } from '../utils' | ||
4 | import { AccountBlock } from '../../../shared/models/blocklist' | ||
5 | |||
6 | enum ScopeNames { | ||
7 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | ||
8 | } | ||
9 | |||
10 | @Scopes({ | ||
11 | [ScopeNames.WITH_ACCOUNTS]: { | ||
12 | include: [ | ||
13 | { | ||
14 | model: () => AccountModel, | ||
15 | required: true, | ||
16 | as: 'ByAccount' | ||
17 | }, | ||
18 | { | ||
19 | model: () => AccountModel, | ||
20 | required: true, | ||
21 | as: 'BlockedAccount' | ||
22 | } | ||
23 | ] | ||
24 | } | ||
25 | }) | ||
26 | |||
27 | @Table({ | ||
28 | tableName: 'accountBlocklist', | ||
29 | indexes: [ | ||
30 | { | ||
31 | fields: [ 'accountId', 'targetAccountId' ], | ||
32 | unique: true | ||
33 | }, | ||
34 | { | ||
35 | fields: [ 'targetAccountId' ] | ||
36 | } | ||
37 | ] | ||
38 | }) | ||
39 | export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | ||
40 | |||
41 | @CreatedAt | ||
42 | createdAt: Date | ||
43 | |||
44 | @UpdatedAt | ||
45 | updatedAt: Date | ||
46 | |||
47 | @ForeignKey(() => AccountModel) | ||
48 | @Column | ||
49 | accountId: number | ||
50 | |||
51 | @BelongsTo(() => AccountModel, { | ||
52 | foreignKey: { | ||
53 | name: 'accountId', | ||
54 | allowNull: false | ||
55 | }, | ||
56 | as: 'ByAccount', | ||
57 | onDelete: 'CASCADE' | ||
58 | }) | ||
59 | ByAccount: AccountModel | ||
60 | |||
61 | @ForeignKey(() => AccountModel) | ||
62 | @Column | ||
63 | targetAccountId: number | ||
64 | |||
65 | @BelongsTo(() => AccountModel, { | ||
66 | foreignKey: { | ||
67 | name: 'targetAccountId', | ||
68 | allowNull: false | ||
69 | }, | ||
70 | as: 'BlockedAccount', | ||
71 | onDelete: 'CASCADE' | ||
72 | }) | ||
73 | BlockedAccount: AccountModel | ||
74 | |||
75 | static loadByAccountAndTarget (accountId: number, targetAccountId: number) { | ||
76 | const query = { | ||
77 | where: { | ||
78 | accountId, | ||
79 | targetAccountId | ||
80 | } | ||
81 | } | ||
82 | |||
83 | return AccountBlocklistModel.findOne(query) | ||
84 | } | ||
85 | |||
86 | static listForApi (accountId: number, start: number, count: number, sort: string) { | ||
87 | const query = { | ||
88 | offset: start, | ||
89 | limit: count, | ||
90 | order: getSort(sort), | ||
91 | where: { | ||
92 | accountId | ||
93 | } | ||
94 | } | ||
95 | |||
96 | return AccountBlocklistModel | ||
97 | .scope([ ScopeNames.WITH_ACCOUNTS ]) | ||
98 | .findAndCountAll(query) | ||
99 | .then(({ rows, count }) => { | ||
100 | return { total: count, data: rows } | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | toFormattedJSON (): AccountBlock { | ||
105 | return { | ||
106 | byAccount: this.ByAccount.toFormattedJSON(), | ||
107 | blockedAccount: this.BlockedAccount.toFormattedJSON(), | ||
108 | createdAt: this.createdAt | ||
109 | } | ||
110 | } | ||
111 | } | ||
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 27c75d886..5a237d733 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> { | |||
248 | displayName: this.getDisplayName(), | 248 | displayName: this.getDisplayName(), |
249 | description: this.description, | 249 | description: this.description, |
250 | createdAt: this.createdAt, | 250 | createdAt: this.createdAt, |
251 | updatedAt: this.updatedAt | 251 | updatedAt: this.updatedAt, |
252 | userId: this.userId ? this.userId : undefined | ||
252 | } | 253 | } |
253 | 254 | ||
254 | return Object.assign(actor, account) | 255 | return Object.assign(actor, account) |
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts new file mode 100644 index 000000000..0476cad9d --- /dev/null +++ b/server/models/account/user-video-history.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from '../video/video' | ||
3 | import { UserModel } from './user' | ||
4 | |||
5 | @Table({ | ||
6 | tableName: 'userVideoHistory', | ||
7 | indexes: [ | ||
8 | { | ||
9 | fields: [ 'userId', 'videoId' ], | ||
10 | unique: true | ||
11 | }, | ||
12 | { | ||
13 | fields: [ 'userId' ] | ||
14 | }, | ||
15 | { | ||
16 | fields: [ 'videoId' ] | ||
17 | } | ||
18 | ] | ||
19 | }) | ||
20 | export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> { | ||
21 | @CreatedAt | ||
22 | createdAt: Date | ||
23 | |||
24 | @UpdatedAt | ||
25 | updatedAt: Date | ||
26 | |||
27 | @AllowNull(false) | ||
28 | @IsInt | ||
29 | @Column | ||
30 | currentTime: number | ||
31 | |||
32 | @ForeignKey(() => VideoModel) | ||
33 | @Column | ||
34 | videoId: number | ||
35 | |||
36 | @BelongsTo(() => VideoModel, { | ||
37 | foreignKey: { | ||
38 | allowNull: false | ||
39 | }, | ||
40 | onDelete: 'CASCADE' | ||
41 | }) | ||
42 | Video: VideoModel | ||
43 | |||
44 | @ForeignKey(() => UserModel) | ||
45 | @Column | ||
46 | userId: number | ||
47 | |||
48 | @BelongsTo(() => UserModel, { | ||
49 | foreignKey: { | ||
50 | allowNull: false | ||
51 | }, | ||
52 | onDelete: 'CASCADE' | ||
53 | }) | ||
54 | User: UserModel | ||
55 | } | ||
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index e56b0bf40..34aafa1a7 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -31,7 +31,8 @@ import { | |||
31 | isUserRoleValid, | 31 | isUserRoleValid, |
32 | isUserUsernameValid, | 32 | isUserUsernameValid, |
33 | isUserVideoQuotaDailyValid, | 33 | isUserVideoQuotaDailyValid, |
34 | isUserVideoQuotaValid | 34 | isUserVideoQuotaValid, |
35 | isUserWebTorrentEnabledValid | ||
35 | } from '../../helpers/custom-validators/users' | 36 | } from '../../helpers/custom-validators/users' |
36 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 37 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
37 | import { OAuthTokenModel } from '../oauth/oauth-token' | 38 | import { OAuthTokenModel } from '../oauth/oauth-token' |
@@ -109,6 +110,12 @@ export class UserModel extends Model<UserModel> { | |||
109 | 110 | ||
110 | @AllowNull(false) | 111 | @AllowNull(false) |
111 | @Default(true) | 112 | @Default(true) |
113 | @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) | ||
114 | @Column | ||
115 | webTorrentEnabled: boolean | ||
116 | |||
117 | @AllowNull(false) | ||
118 | @Default(true) | ||
112 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) | 119 | @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) |
113 | @Column | 120 | @Column |
114 | autoPlayVideo: boolean | 121 | autoPlayVideo: boolean |
@@ -181,7 +188,25 @@ export class UserModel extends Model<UserModel> { | |||
181 | return this.count() | 188 | return this.count() |
182 | } | 189 | } |
183 | 190 | ||
184 | static listForApi (start: number, count: number, sort: string) { | 191 | static listForApi (start: number, count: number, sort: string, search?: string) { |
192 | let where = undefined | ||
193 | if (search) { | ||
194 | where = { | ||
195 | [Sequelize.Op.or]: [ | ||
196 | { | ||
197 | email: { | ||
198 | [Sequelize.Op.iLike]: '%' + search + '%' | ||
199 | } | ||
200 | }, | ||
201 | { | ||
202 | username: { | ||
203 | [ Sequelize.Op.iLike ]: '%' + search + '%' | ||
204 | } | ||
205 | } | ||
206 | ] | ||
207 | } | ||
208 | } | ||
209 | |||
185 | const query = { | 210 | const query = { |
186 | attributes: { | 211 | attributes: { |
187 | include: [ | 212 | include: [ |
@@ -204,7 +229,8 @@ export class UserModel extends Model<UserModel> { | |||
204 | }, | 229 | }, |
205 | offset: start, | 230 | offset: start, |
206 | limit: count, | 231 | limit: count, |
207 | order: getSort(sort) | 232 | order: getSort(sort), |
233 | where | ||
208 | } | 234 | } |
209 | 235 | ||
210 | return UserModel.findAndCountAll(query) | 236 | return UserModel.findAndCountAll(query) |
@@ -336,6 +362,7 @@ export class UserModel extends Model<UserModel> { | |||
336 | email: this.email, | 362 | email: this.email, |
337 | emailVerified: this.emailVerified, | 363 | emailVerified: this.emailVerified, |
338 | nsfwPolicy: this.nsfwPolicy, | 364 | nsfwPolicy: this.nsfwPolicy, |
365 | webTorrentEnabled: this.webTorrentEnabled, | ||
339 | autoPlayVideo: this.autoPlayVideo, | 366 | autoPlayVideo: this.autoPlayVideo, |
340 | role: this.role, | 367 | role: this.role, |
341 | roleLabel: USER_ROLE_LABELS[ this.role ], | 368 | roleLabel: USER_ROLE_LABELS[ this.role ], |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 27bb43dae..3373355ef 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -280,7 +280,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
280 | return ActorFollowModel.findAll(query) | 280 | return ActorFollowModel.findAll(query) |
281 | } | 281 | } |
282 | 282 | ||
283 | static listFollowingForApi (id: number, start: number, count: number, sort: string) { | 283 | static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) { |
284 | const query = { | 284 | const query = { |
285 | distinct: true, | 285 | distinct: true, |
286 | offset: start, | 286 | offset: start, |
@@ -299,7 +299,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
299 | model: ActorModel, | 299 | model: ActorModel, |
300 | as: 'ActorFollowing', | 300 | as: 'ActorFollowing', |
301 | required: true, | 301 | required: true, |
302 | include: [ ServerModel ] | 302 | include: [ |
303 | { | ||
304 | model: ServerModel, | ||
305 | required: true, | ||
306 | where: search ? { | ||
307 | host: { | ||
308 | [Sequelize.Op.iLike]: '%' + search + '%' | ||
309 | } | ||
310 | } : undefined | ||
311 | } | ||
312 | ] | ||
303 | } | 313 | } |
304 | ] | 314 | ] |
305 | } | 315 | } |
@@ -313,6 +323,49 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
313 | }) | 323 | }) |
314 | } | 324 | } |
315 | 325 | ||
326 | static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { | ||
327 | const query = { | ||
328 | distinct: true, | ||
329 | offset: start, | ||
330 | limit: count, | ||
331 | order: getSort(sort), | ||
332 | include: [ | ||
333 | { | ||
334 | model: ActorModel, | ||
335 | required: true, | ||
336 | as: 'ActorFollower', | ||
337 | include: [ | ||
338 | { | ||
339 | model: ServerModel, | ||
340 | required: true, | ||
341 | where: search ? { | ||
342 | host: { | ||
343 | [ Sequelize.Op.iLike ]: '%' + search + '%' | ||
344 | } | ||
345 | } : undefined | ||
346 | } | ||
347 | ] | ||
348 | }, | ||
349 | { | ||
350 | model: ActorModel, | ||
351 | as: 'ActorFollowing', | ||
352 | required: true, | ||
353 | where: { | ||
354 | id | ||
355 | } | ||
356 | } | ||
357 | ] | ||
358 | } | ||
359 | |||
360 | return ActorFollowModel.findAndCountAll(query) | ||
361 | .then(({ rows, count }) => { | ||
362 | return { | ||
363 | data: rows, | ||
364 | total: count | ||
365 | } | ||
366 | }) | ||
367 | } | ||
368 | |||
316 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | 369 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { |
317 | const query = { | 370 | const query = { |
318 | attributes: [], | 371 | attributes: [], |
@@ -370,39 +423,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
370 | }) | 423 | }) |
371 | } | 424 | } |
372 | 425 | ||
373 | static listFollowersForApi (id: number, start: number, count: number, sort: string) { | ||
374 | const query = { | ||
375 | distinct: true, | ||
376 | offset: start, | ||
377 | limit: count, | ||
378 | order: getSort(sort), | ||
379 | include: [ | ||
380 | { | ||
381 | model: ActorModel, | ||
382 | required: true, | ||
383 | as: 'ActorFollower', | ||
384 | include: [ ServerModel ] | ||
385 | }, | ||
386 | { | ||
387 | model: ActorModel, | ||
388 | as: 'ActorFollowing', | ||
389 | required: true, | ||
390 | where: { | ||
391 | id | ||
392 | } | ||
393 | } | ||
394 | ] | ||
395 | } | ||
396 | |||
397 | return ActorFollowModel.findAndCountAll(query) | ||
398 | .then(({ rows, count }) => { | ||
399 | return { | ||
400 | data: rows, | ||
401 | total: count | ||
402 | } | ||
403 | }) | ||
404 | } | ||
405 | |||
406 | static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { | 426 | static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { |
407 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) | 427 | return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) |
408 | } | 428 | } |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 2ebe23ef1..cbfc7f7fa 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -408,6 +408,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
408 | url: { | 408 | url: { |
409 | type: 'Link', | 409 | type: 'Link', |
410 | mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 410 | mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, |
411 | mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, | ||
411 | href: this.fileUrl, | 412 | href: this.fileUrl, |
412 | height: this.VideoFile.resolution, | 413 | height: this.VideoFile.resolution, |
413 | size: this.VideoFile.size, | 414 | size: this.VideoFile.size, |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts new file mode 100644 index 000000000..450f27152 --- /dev/null +++ b/server/models/server/server-blocklist.ts | |||
@@ -0,0 +1,121 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from '../account/account' | ||
3 | import { ServerModel } from './server' | ||
4 | import { ServerBlock } from '../../../shared/models/blocklist' | ||
5 | import { getSort } from '../utils' | ||
6 | |||
7 | enum ScopeNames { | ||
8 | WITH_ACCOUNT = 'WITH_ACCOUNT', | ||
9 | WITH_SERVER = 'WITH_SERVER' | ||
10 | } | ||
11 | |||
12 | @Scopes({ | ||
13 | [ScopeNames.WITH_ACCOUNT]: { | ||
14 | include: [ | ||
15 | { | ||
16 | model: () => AccountModel, | ||
17 | required: true | ||
18 | } | ||
19 | ] | ||
20 | }, | ||
21 | [ScopeNames.WITH_SERVER]: { | ||
22 | include: [ | ||
23 | { | ||
24 | model: () => ServerModel, | ||
25 | required: true | ||
26 | } | ||
27 | ] | ||
28 | } | ||
29 | }) | ||
30 | |||
31 | @Table({ | ||
32 | tableName: 'serverBlocklist', | ||
33 | indexes: [ | ||
34 | { | ||
35 | fields: [ 'accountId', 'targetServerId' ], | ||
36 | unique: true | ||
37 | }, | ||
38 | { | ||
39 | fields: [ 'targetServerId' ] | ||
40 | } | ||
41 | ] | ||
42 | }) | ||
43 | export class ServerBlocklistModel extends Model<ServerBlocklistModel> { | ||
44 | |||
45 | @CreatedAt | ||
46 | createdAt: Date | ||
47 | |||
48 | @UpdatedAt | ||
49 | updatedAt: Date | ||
50 | |||
51 | @ForeignKey(() => AccountModel) | ||
52 | @Column | ||
53 | accountId: number | ||
54 | |||
55 | @BelongsTo(() => AccountModel, { | ||
56 | foreignKey: { | ||
57 | name: 'accountId', | ||
58 | allowNull: false | ||
59 | }, | ||
60 | onDelete: 'CASCADE' | ||
61 | }) | ||
62 | ByAccount: AccountModel | ||
63 | |||
64 | @ForeignKey(() => ServerModel) | ||
65 | @Column | ||
66 | targetServerId: number | ||
67 | |||
68 | @BelongsTo(() => ServerModel, { | ||
69 | foreignKey: { | ||
70 | name: 'targetServerId', | ||
71 | allowNull: false | ||
72 | }, | ||
73 | onDelete: 'CASCADE' | ||
74 | }) | ||
75 | BlockedServer: ServerModel | ||
76 | |||
77 | static loadByAccountAndHost (accountId: number, host: string) { | ||
78 | const query = { | ||
79 | where: { | ||
80 | accountId | ||
81 | }, | ||
82 | include: [ | ||
83 | { | ||
84 | model: ServerModel, | ||
85 | where: { | ||
86 | host | ||
87 | }, | ||
88 | required: true | ||
89 | } | ||
90 | ] | ||
91 | } | ||
92 | |||
93 | return ServerBlocklistModel.findOne(query) | ||
94 | } | ||
95 | |||
96 | static listForApi (accountId: number, start: number, count: number, sort: string) { | ||
97 | const query = { | ||
98 | offset: start, | ||
99 | limit: count, | ||
100 | order: getSort(sort), | ||
101 | where: { | ||
102 | accountId | ||
103 | } | ||
104 | } | ||
105 | |||
106 | return ServerBlocklistModel | ||
107 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) | ||
108 | .findAndCountAll(query) | ||
109 | .then(({ rows, count }) => { | ||
110 | return { total: count, data: rows } | ||
111 | }) | ||
112 | } | ||
113 | |||
114 | toFormattedJSON (): ServerBlock { | ||
115 | return { | ||
116 | byAccount: this.ByAccount.toFormattedJSON(), | ||
117 | blockedServer: this.BlockedServer.toFormattedJSON(), | ||
118 | createdAt: this.createdAt | ||
119 | } | ||
120 | } | ||
121 | } | ||
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ca3b24d51..300d70938 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> { | |||
49 | 49 | ||
50 | return ServerModel.findOne(query) | 50 | return ServerModel.findOne(query) |
51 | } | 51 | } |
52 | |||
53 | toFormattedJSON () { | ||
54 | return { | ||
55 | host: this.host | ||
56 | } | ||
57 | } | ||
52 | } | 58 | } |
diff --git a/server/models/utils.ts b/server/models/utils.ts index e0bf091ad..60b0906e8 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -64,9 +64,25 @@ function createSimilarityAttribute (col: string, value: string) { | |||
64 | ) | 64 | ) |
65 | } | 65 | } |
66 | 66 | ||
67 | function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { | ||
68 | const blockerIds = [ serverAccountId ] | ||
69 | if (userAccountId) blockerIds.push(userAccountId) | ||
70 | |||
71 | const blockerIdsString = blockerIds.join(', ') | ||
72 | |||
73 | const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
74 | ' UNION ALL ' + | ||
75 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
76 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
77 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
78 | |||
79 | return query | ||
80 | } | ||
81 | |||
67 | // --------------------------------------------------------------------------- | 82 | // --------------------------------------------------------------------------- |
68 | 83 | ||
69 | export { | 84 | export { |
85 | buildBlockedAccountSQL, | ||
70 | SortType, | 86 | SortType, |
71 | getSort, | 87 | getSort, |
72 | getVideoSort, | 88 | getVideoSort, |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index f84c1880c..dd6d08139 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,6 +1,17 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, | 3 | AllowNull, |
4 | BeforeDestroy, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | IFindOptions, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
4 | UpdatedAt | 15 | UpdatedAt |
5 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
6 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' | 17 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' |
@@ -13,9 +24,11 @@ import { AccountModel } from '../account/account' | |||
13 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
14 | import { AvatarModel } from '../avatar/avatar' | 25 | import { AvatarModel } from '../avatar/avatar' |
15 | import { ServerModel } from '../server/server' | 26 | import { ServerModel } from '../server/server' |
16 | import { getSort, throwIfNotValid } from '../utils' | 27 | import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' |
17 | import { VideoModel } from './video' | 28 | import { VideoModel } from './video' |
18 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | ||
31 | import { UserModel } from '../account/user' | ||
19 | 32 | ||
20 | enum ScopeNames { | 33 | enum ScopeNames { |
21 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 34 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -25,18 +38,29 @@ enum ScopeNames { | |||
25 | } | 38 | } |
26 | 39 | ||
27 | @Scopes({ | 40 | @Scopes({ |
28 | [ScopeNames.ATTRIBUTES_FOR_API]: { | 41 | [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { |
29 | attributes: { | 42 | return { |
30 | include: [ | 43 | attributes: { |
31 | [ | 44 | include: [ |
32 | Sequelize.literal( | 45 | [ |
33 | '(SELECT COUNT("replies"."id") ' + | 46 | Sequelize.literal( |
34 | 'FROM "videoComment" AS "replies" ' + | 47 | '(' + |
35 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' | 48 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + |
36 | ), | 49 | 'SELECT COUNT("replies"."id") - (' + |
37 | 'totalReplies' | 50 | 'SELECT COUNT("replies"."id") ' + |
51 | 'FROM "videoComment" AS "replies" ' + | ||
52 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
53 | 'AND "accountId" IN (SELECT "id" FROM "blocklist")' + | ||
54 | ')' + | ||
55 | 'FROM "videoComment" AS "replies" ' + | ||
56 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
57 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
58 | ')' | ||
59 | ), | ||
60 | 'totalReplies' | ||
61 | ] | ||
38 | ] | 62 | ] |
39 | ] | 63 | } |
40 | } | 64 | } |
41 | }, | 65 | }, |
42 | [ScopeNames.WITH_ACCOUNT]: { | 66 | [ScopeNames.WITH_ACCOUNT]: { |
@@ -267,26 +291,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
267 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) | 291 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) |
268 | } | 292 | } |
269 | 293 | ||
270 | static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { | 294 | static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) { |
295 | const serverActor = await getServerActor() | ||
296 | const serverAccountId = serverActor.Account.id | ||
297 | const userAccountId = user ? user.Account.id : undefined | ||
298 | |||
271 | const query = { | 299 | const query = { |
272 | offset: start, | 300 | offset: start, |
273 | limit: count, | 301 | limit: count, |
274 | order: getSort(sort), | 302 | order: getSort(sort), |
275 | where: { | 303 | where: { |
276 | videoId, | 304 | videoId, |
277 | inReplyToCommentId: null | 305 | inReplyToCommentId: null, |
306 | accountId: { | ||
307 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
308 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
309 | ) | ||
310 | } | ||
278 | } | 311 | } |
279 | } | 312 | } |
280 | 313 | ||
314 | // FIXME: typings | ||
315 | const scopes: any[] = [ | ||
316 | ScopeNames.WITH_ACCOUNT, | ||
317 | { | ||
318 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
319 | } | ||
320 | ] | ||
321 | |||
281 | return VideoCommentModel | 322 | return VideoCommentModel |
282 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 323 | .scope(scopes) |
283 | .findAndCountAll(query) | 324 | .findAndCountAll(query) |
284 | .then(({ rows, count }) => { | 325 | .then(({ rows, count }) => { |
285 | return { total: count, data: rows } | 326 | return { total: count, data: rows } |
286 | }) | 327 | }) |
287 | } | 328 | } |
288 | 329 | ||
289 | static listThreadCommentsForApi (videoId: number, threadId: number) { | 330 | static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) { |
331 | const serverActor = await getServerActor() | ||
332 | const serverAccountId = serverActor.Account.id | ||
333 | const userAccountId = user ? user.Account.id : undefined | ||
334 | |||
290 | const query = { | 335 | const query = { |
291 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], | 336 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], |
292 | where: { | 337 | where: { |
@@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
294 | [ Sequelize.Op.or ]: [ | 339 | [ Sequelize.Op.or ]: [ |
295 | { id: threadId }, | 340 | { id: threadId }, |
296 | { originCommentId: threadId } | 341 | { originCommentId: threadId } |
297 | ] | 342 | ], |
343 | accountId: { | ||
344 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
345 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
346 | ) | ||
347 | } | ||
298 | } | 348 | } |
299 | } | 349 | } |
300 | 350 | ||
351 | const scopes: any[] = [ | ||
352 | ScopeNames.WITH_ACCOUNT, | ||
353 | { | ||
354 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
355 | } | ||
356 | ] | ||
357 | |||
301 | return VideoCommentModel | 358 | return VideoCommentModel |
302 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 359 | .scope(scopes) |
303 | .findAndCountAll(query) | 360 | .findAndCountAll(query) |
304 | .then(({ rows, count }) => { | 361 | .then(({ rows, count }) => { |
305 | return { total: count, data: rows } | 362 | return { total: count, data: rows } |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index f23dde9b8..e3f8d525b 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | getVideoLikesActivityPubUrl, | 10 | getVideoLikesActivityPubUrl, |
11 | getVideoSharesActivityPubUrl | 11 | getVideoSharesActivityPubUrl |
12 | } from '../../lib/activitypub' | 12 | } from '../../lib/activitypub' |
13 | import { isArray } from '../../helpers/custom-validators/misc' | ||
13 | 14 | ||
14 | export type VideoFormattingJSONOptions = { | 15 | export type VideoFormattingJSONOptions = { |
15 | completeDescription?: boolean | 16 | completeDescription?: boolean |
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting | |||
24 | const formattedAccount = video.VideoChannel.Account.toFormattedJSON() | 25 | const formattedAccount = video.VideoChannel.Account.toFormattedJSON() |
25 | const formattedVideoChannel = video.VideoChannel.toFormattedJSON() | 26 | const formattedVideoChannel = video.VideoChannel.toFormattedJSON() |
26 | 27 | ||
28 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | ||
29 | |||
27 | const videoObject: Video = { | 30 | const videoObject: Video = { |
28 | id: video.id, | 31 | id: video.id, |
29 | uuid: video.uuid, | 32 | uuid: video.uuid, |
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting | |||
74 | url: formattedVideoChannel.url, | 77 | url: formattedVideoChannel.url, |
75 | host: formattedVideoChannel.host, | 78 | host: formattedVideoChannel.host, |
76 | avatar: formattedVideoChannel.avatar | 79 | avatar: formattedVideoChannel.avatar |
77 | } | 80 | }, |
81 | |||
82 | userHistory: userHistory ? { | ||
83 | currentTime: userHistory.currentTime | ||
84 | } : undefined | ||
78 | } | 85 | } |
79 | 86 | ||
80 | if (options) { | 87 | if (options) { |
@@ -201,6 +208,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
201 | url.push({ | 208 | url.push({ |
202 | type: 'Link', | 209 | type: 'Link', |
203 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | 210 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, |
211 | mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | ||
204 | href: video.getVideoFileUrl(file, baseUrlHttp), | 212 | href: video.getVideoFileUrl(file, baseUrlHttp), |
205 | height: file.resolution, | 213 | height: file.resolution, |
206 | size: file.size, | 214 | size: file.size, |
@@ -210,6 +218,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
210 | url.push({ | 218 | url.push({ |
211 | type: 'Link', | 219 | type: 'Link', |
212 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | 220 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', |
221 | mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
213 | href: video.getTorrentUrl(file, baseUrlHttp), | 222 | href: video.getTorrentUrl(file, baseUrlHttp), |
214 | height: file.resolution | 223 | height: file.resolution |
215 | }) | 224 | }) |
@@ -217,6 +226,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
217 | url.push({ | 226 | url.push({ |
218 | type: 'Link', | 227 | type: 'Link', |
219 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | 228 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', |
229 | mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
220 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | 230 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), |
221 | height: file.resolution | 231 | height: file.resolution |
222 | }) | 232 | }) |
@@ -226,6 +236,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
226 | url.push({ | 236 | url.push({ |
227 | type: 'Link', | 237 | type: 'Link', |
228 | mimeType: 'text/html', | 238 | mimeType: 'text/html', |
239 | mediaType: 'text/html', | ||
229 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 240 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
230 | }) | 241 | }) |
231 | 242 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 6c89c16bf..6c183933b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -27,7 +27,7 @@ import { | |||
27 | Table, | 27 | Table, |
28 | UpdatedAt | 28 | UpdatedAt |
29 | } from 'sequelize-typescript' | 29 | } from 'sequelize-typescript' |
30 | import { VideoPrivacy, VideoState } from '../../../shared' | 30 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' |
31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate' | |||
70 | import { ActorModel } from '../activitypub/actor' | 70 | import { ActorModel } from '../activitypub/actor' |
71 | import { AvatarModel } from '../avatar/avatar' | 71 | import { AvatarModel } from '../avatar/avatar' |
72 | import { ServerModel } from '../server/server' | 72 | import { ServerModel } from '../server/server' |
73 | import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' | 73 | import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' |
74 | import { TagModel } from './tag' | 74 | import { TagModel } from './tag' |
75 | import { VideoAbuseModel } from './video-abuse' | 75 | import { VideoAbuseModel } from './video-abuse' |
76 | import { VideoChannelModel } from './video-channel' | 76 | import { VideoChannelModel } from './video-channel' |
@@ -92,6 +92,8 @@ import { | |||
92 | videoModelToFormattedJSON | 92 | videoModelToFormattedJSON |
93 | } from './video-format-utils' | 93 | } from './video-format-utils' |
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
96 | import { UserModel } from '../account/user' | ||
95 | 97 | ||
96 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
97 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 99 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -127,7 +129,8 @@ export enum ScopeNames { | |||
127 | WITH_TAGS = 'WITH_TAGS', | 129 | WITH_TAGS = 'WITH_TAGS', |
128 | WITH_FILES = 'WITH_FILES', | 130 | WITH_FILES = 'WITH_FILES', |
129 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 131 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
130 | WITH_BLACKLISTED = 'WITH_BLACKLISTED' | 132 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
133 | WITH_USER_HISTORY = 'WITH_USER_HISTORY' | ||
131 | } | 134 | } |
132 | 135 | ||
133 | type ForAPIOptions = { | 136 | type ForAPIOptions = { |
@@ -136,6 +139,7 @@ type ForAPIOptions = { | |||
136 | } | 139 | } |
137 | 140 | ||
138 | type AvailableForListIDsOptions = { | 141 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | ||
139 | actorId: number | 143 | actorId: number |
140 | includeLocalVideos: boolean | 144 | includeLocalVideos: boolean |
141 | filter?: VideoFilter | 145 | filter?: VideoFilter |
@@ -149,6 +153,7 @@ type AvailableForListIDsOptions = { | |||
149 | accountId?: number | 153 | accountId?: number |
150 | videoChannelId?: number | 154 | videoChannelId?: number |
151 | trendingDays?: number | 155 | trendingDays?: number |
156 | user?: UserModel | ||
152 | } | 157 | } |
153 | 158 | ||
154 | @Scopes({ | 159 | @Scopes({ |
@@ -234,6 +239,22 @@ type AvailableForListIDsOptions = { | |||
234 | } | 239 | } |
235 | ] | 240 | ] |
236 | }, | 241 | }, |
242 | channelId: { | ||
243 | [ Sequelize.Op.notIn ]: Sequelize.literal( | ||
244 | '(' + | ||
245 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + | ||
246 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + | ||
247 | ')' + | ||
248 | ')' | ||
249 | ) | ||
250 | } | ||
251 | }, | ||
252 | include: [] | ||
253 | } | ||
254 | |||
255 | // Only list public/published videos | ||
256 | if (!options.filter || options.filter !== 'all-local') { | ||
257 | const privacyWhere = { | ||
237 | // Always list public videos | 258 | // Always list public videos |
238 | privacy: VideoPrivacy.PUBLIC, | 259 | privacy: VideoPrivacy.PUBLIC, |
239 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding | 260 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding |
@@ -248,8 +269,9 @@ type AvailableForListIDsOptions = { | |||
248 | } | 269 | } |
249 | } | 270 | } |
250 | ] | 271 | ] |
251 | }, | 272 | } |
252 | include: [] | 273 | |
274 | Object.assign(query.where, privacyWhere) | ||
253 | } | 275 | } |
254 | 276 | ||
255 | if (options.filter || options.accountId || options.videoChannelId) { | 277 | if (options.filter || options.accountId || options.videoChannelId) { |
@@ -464,6 +486,8 @@ type AvailableForListIDsOptions = { | |||
464 | include: [ | 486 | include: [ |
465 | { | 487 | { |
466 | model: () => VideoFileModel.unscoped(), | 488 | model: () => VideoFileModel.unscoped(), |
489 | // FIXME: typings | ||
490 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | ||
467 | required: false, | 491 | required: false, |
468 | include: [ | 492 | include: [ |
469 | { | 493 | { |
@@ -482,6 +506,20 @@ type AvailableForListIDsOptions = { | |||
482 | required: false | 506 | required: false |
483 | } | 507 | } |
484 | ] | 508 | ] |
509 | }, | ||
510 | [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { | ||
511 | return { | ||
512 | include: [ | ||
513 | { | ||
514 | attributes: [ 'currentTime' ], | ||
515 | model: UserVideoHistoryModel.unscoped(), | ||
516 | required: false, | ||
517 | where: { | ||
518 | userId | ||
519 | } | ||
520 | } | ||
521 | ] | ||
522 | } | ||
485 | } | 523 | } |
486 | }) | 524 | }) |
487 | @Table({ | 525 | @Table({ |
@@ -672,11 +710,19 @@ export class VideoModel extends Model<VideoModel> { | |||
672 | name: 'videoId', | 710 | name: 'videoId', |
673 | allowNull: false | 711 | allowNull: false |
674 | }, | 712 | }, |
675 | onDelete: 'cascade', | 713 | onDelete: 'cascade' |
676 | hooks: true | ||
677 | }) | 714 | }) |
678 | VideoViews: VideoViewModel[] | 715 | VideoViews: VideoViewModel[] |
679 | 716 | ||
717 | @HasMany(() => UserVideoHistoryModel, { | ||
718 | foreignKey: { | ||
719 | name: 'videoId', | ||
720 | allowNull: false | ||
721 | }, | ||
722 | onDelete: 'cascade' | ||
723 | }) | ||
724 | UserVideoHistories: UserVideoHistoryModel[] | ||
725 | |||
680 | @HasOne(() => ScheduleVideoUpdateModel, { | 726 | @HasOne(() => ScheduleVideoUpdateModel, { |
681 | foreignKey: { | 727 | foreignKey: { |
682 | name: 'videoId', | 728 | name: 'videoId', |
@@ -762,6 +808,16 @@ export class VideoModel extends Model<VideoModel> { | |||
762 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll() | 808 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll() |
763 | } | 809 | } |
764 | 810 | ||
811 | static listLocal () { | ||
812 | const query = { | ||
813 | where: { | ||
814 | remote: false | ||
815 | } | ||
816 | } | ||
817 | |||
818 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) | ||
819 | } | ||
820 | |||
765 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 821 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
766 | function getRawQuery (select: string) { | 822 | function getRawQuery (select: string) { |
767 | const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + | 823 | const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + |
@@ -930,8 +986,13 @@ export class VideoModel extends Model<VideoModel> { | |||
930 | accountId?: number, | 986 | accountId?: number, |
931 | videoChannelId?: number, | 987 | videoChannelId?: number, |
932 | actorId?: number | 988 | actorId?: number |
933 | trendingDays?: number | 989 | trendingDays?: number, |
990 | user?: UserModel | ||
934 | }, countVideos = true) { | 991 | }, countVideos = true) { |
992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | ||
993 | throw new Error('Try to filter all-local but no user has not the see all videos right') | ||
994 | } | ||
995 | |||
935 | const query: IFindOptions<VideoModel> = { | 996 | const query: IFindOptions<VideoModel> = { |
936 | offset: options.start, | 997 | offset: options.start, |
937 | limit: options.count, | 998 | limit: options.count, |
@@ -945,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> { | |||
945 | query.group = 'VideoModel.id' | 1006 | query.group = 'VideoModel.id' |
946 | } | 1007 | } |
947 | 1008 | ||
1009 | const serverActor = await getServerActor() | ||
1010 | |||
948 | // actorId === null has a meaning, so just check undefined | 1011 | // actorId === null has a meaning, so just check undefined |
949 | const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id | 1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id |
950 | 1013 | ||
951 | const queryOptions = { | 1014 | const queryOptions = { |
952 | actorId, | 1015 | actorId, |
1016 | serverAccountId: serverActor.Account.id, | ||
953 | nsfw: options.nsfw, | 1017 | nsfw: options.nsfw, |
954 | categoryOneOf: options.categoryOneOf, | 1018 | categoryOneOf: options.categoryOneOf, |
955 | licenceOneOf: options.licenceOneOf, | 1019 | licenceOneOf: options.licenceOneOf, |
@@ -961,6 +1025,7 @@ export class VideoModel extends Model<VideoModel> { | |||
961 | accountId: options.accountId, | 1025 | accountId: options.accountId, |
962 | videoChannelId: options.videoChannelId, | 1026 | videoChannelId: options.videoChannelId, |
963 | includeLocalVideos: options.includeLocalVideos, | 1027 | includeLocalVideos: options.includeLocalVideos, |
1028 | user: options.user, | ||
964 | trendingDays | 1029 | trendingDays |
965 | } | 1030 | } |
966 | 1031 | ||
@@ -983,6 +1048,8 @@ export class VideoModel extends Model<VideoModel> { | |||
983 | tagsAllOf?: string[] | 1048 | tagsAllOf?: string[] |
984 | durationMin?: number // seconds | 1049 | durationMin?: number // seconds |
985 | durationMax?: number // seconds | 1050 | durationMax?: number // seconds |
1051 | user?: UserModel, | ||
1052 | filter?: VideoFilter | ||
986 | }) { | 1053 | }) { |
987 | const whereAnd = [] | 1054 | const whereAnd = [] |
988 | 1055 | ||
@@ -1052,13 +1119,16 @@ export class VideoModel extends Model<VideoModel> { | |||
1052 | const serverActor = await getServerActor() | 1119 | const serverActor = await getServerActor() |
1053 | const queryOptions = { | 1120 | const queryOptions = { |
1054 | actorId: serverActor.id, | 1121 | actorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | ||
1055 | includeLocalVideos: options.includeLocalVideos, | 1123 | includeLocalVideos: options.includeLocalVideos, |
1056 | nsfw: options.nsfw, | 1124 | nsfw: options.nsfw, |
1057 | categoryOneOf: options.categoryOneOf, | 1125 | categoryOneOf: options.categoryOneOf, |
1058 | licenceOneOf: options.licenceOneOf, | 1126 | licenceOneOf: options.licenceOneOf, |
1059 | languageOneOf: options.languageOneOf, | 1127 | languageOneOf: options.languageOneOf, |
1060 | tagsOneOf: options.tagsOneOf, | 1128 | tagsOneOf: options.tagsOneOf, |
1061 | tagsAllOf: options.tagsAllOf | 1129 | tagsAllOf: options.tagsAllOf, |
1130 | user: options.user, | ||
1131 | filter: options.filter | ||
1062 | } | 1132 | } |
1063 | 1133 | ||
1064 | return VideoModel.getAvailableForApi(query, queryOptions) | 1134 | return VideoModel.getAvailableForApi(query, queryOptions) |
@@ -1125,7 +1195,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1125 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | 1195 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) |
1126 | } | 1196 | } |
1127 | 1197 | ||
1128 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { | 1198 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { |
1129 | const where = VideoModel.buildWhereIdOrUUID(id) | 1199 | const where = VideoModel.buildWhereIdOrUUID(id) |
1130 | 1200 | ||
1131 | const options = { | 1201 | const options = { |
@@ -1134,14 +1204,20 @@ export class VideoModel extends Model<VideoModel> { | |||
1134 | transaction: t | 1204 | transaction: t |
1135 | } | 1205 | } |
1136 | 1206 | ||
1207 | const scopes = [ | ||
1208 | ScopeNames.WITH_TAGS, | ||
1209 | ScopeNames.WITH_BLACKLISTED, | ||
1210 | ScopeNames.WITH_FILES, | ||
1211 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1212 | ScopeNames.WITH_SCHEDULED_UPDATE | ||
1213 | ] | ||
1214 | |||
1215 | if (userId) { | ||
1216 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings | ||
1217 | } | ||
1218 | |||
1137 | return VideoModel | 1219 | return VideoModel |
1138 | .scope([ | 1220 | .scope(scopes) |
1139 | ScopeNames.WITH_TAGS, | ||
1140 | ScopeNames.WITH_BLACKLISTED, | ||
1141 | ScopeNames.WITH_FILES, | ||
1142 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1143 | ScopeNames.WITH_SCHEDULED_UPDATE | ||
1144 | ]) | ||
1145 | .findOne(options) | 1221 | .findOne(options) |
1146 | } | 1222 | } |
1147 | 1223 | ||
@@ -1179,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1179 | 1255 | ||
1180 | // threshold corresponds to how many video the field should have to be returned | 1256 | // threshold corresponds to how many video the field should have to be returned |
1181 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1257 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1182 | const actorId = (await getServerActor()).id | 1258 | const serverActor = await getServerActor() |
1259 | const actorId = serverActor.id | ||
1183 | 1260 | ||
1184 | const scopeOptions = { | 1261 | const scopeOptions: AvailableForListIDsOptions = { |
1262 | serverAccountId: serverActor.Account.id, | ||
1185 | actorId, | 1263 | actorId, |
1186 | includeLocalVideos: true | 1264 | includeLocalVideos: true |
1187 | } | 1265 | } |
@@ -1216,7 +1294,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1216 | } | 1294 | } |
1217 | 1295 | ||
1218 | private static buildActorWhereWithFilter (filter?: VideoFilter) { | 1296 | private static buildActorWhereWithFilter (filter?: VideoFilter) { |
1219 | if (filter && filter === 'local') { | 1297 | if (filter && (filter === 'local' || filter === 'all-local')) { |
1220 | return { | 1298 | return { |
1221 | serverId: null | 1299 | serverId: null |
1222 | } | 1300 | } |
@@ -1225,7 +1303,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1225 | return {} | 1303 | return {} |
1226 | } | 1304 | } |
1227 | 1305 | ||
1228 | private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { | 1306 | private static async getAvailableForApi ( |
1307 | query: IFindOptions<VideoModel>, | ||
1308 | options: AvailableForListIDsOptions, | ||
1309 | countVideos = true | ||
1310 | ) { | ||
1229 | const idsScope = { | 1311 | const idsScope = { |
1230 | method: [ | 1312 | method: [ |
1231 | ScopeNames.AVAILABLE_FOR_LIST_IDS, options | 1313 | ScopeNames.AVAILABLE_FOR_LIST_IDS, options |
@@ -1249,8 +1331,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1249 | 1331 | ||
1250 | if (ids.length === 0) return { data: [], total: count } | 1332 | if (ids.length === 0) return { data: [], total: count } |
1251 | 1333 | ||
1252 | const apiScope = { | 1334 | // FIXME: typings |
1253 | method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] | 1335 | const apiScope: any[] = [ |
1336 | { | ||
1337 | method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] | ||
1338 | } | ||
1339 | ] | ||
1340 | |||
1341 | if (options.user) { | ||
1342 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | ||
1254 | } | 1343 | } |
1255 | 1344 | ||
1256 | const secondQuery = { | 1345 | const secondQuery = { |
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts new file mode 100644 index 000000000..c745ac975 --- /dev/null +++ b/server/tests/api/check-params/blocklist.ts | |||
@@ -0,0 +1,494 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | killallServers, | ||
11 | makeDeleteRequest, | ||
12 | makeGetRequest, | ||
13 | makePostBodyRequest, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, userLogin | ||
16 | } from '../../utils' | ||
17 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | ||
18 | |||
19 | describe('Test blocklist API validators', function () { | ||
20 | let servers: ServerInfo[] | ||
21 | let server: ServerInfo | ||
22 | let userAccessToken: string | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(60000) | ||
26 | |||
27 | await flushTests() | ||
28 | |||
29 | servers = await flushAndRunMultipleServers(2) | ||
30 | await setAccessTokensToServers(servers) | ||
31 | |||
32 | server = servers[0] | ||
33 | |||
34 | const user = { username: 'user1', password: 'password' } | ||
35 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
36 | |||
37 | userAccessToken = await userLogin(server, user) | ||
38 | |||
39 | await doubleFollow(servers[0], servers[1]) | ||
40 | }) | ||
41 | |||
42 | // --------------------------------------------------------------- | ||
43 | |||
44 | describe('When managing user blocklist', function () { | ||
45 | |||
46 | describe('When managing user accounts blocklist', function () { | ||
47 | const path = '/api/v1/users/me/blocklist/accounts' | ||
48 | |||
49 | describe('When listing blocked accounts', function () { | ||
50 | it('Should fail with an unauthenticated user', async function () { | ||
51 | await makeGetRequest({ | ||
52 | url: server.url, | ||
53 | path, | ||
54 | statusCodeExpected: 401 | ||
55 | }) | ||
56 | }) | ||
57 | |||
58 | it('Should fail with a bad start pagination', async function () { | ||
59 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
60 | }) | ||
61 | |||
62 | it('Should fail with a bad count pagination', async function () { | ||
63 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
64 | }) | ||
65 | |||
66 | it('Should fail with an incorrect sort', async function () { | ||
67 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | describe('When blocking an account', function () { | ||
72 | it('Should fail with an unauthenticated user', async function () { | ||
73 | await makePostBodyRequest({ | ||
74 | url: server.url, | ||
75 | path, | ||
76 | fields: { accountName: 'user1' }, | ||
77 | statusCodeExpected: 401 | ||
78 | }) | ||
79 | }) | ||
80 | |||
81 | it('Should fail with an unknown account', async function () { | ||
82 | await makePostBodyRequest({ | ||
83 | url: server.url, | ||
84 | token: server.accessToken, | ||
85 | path, | ||
86 | fields: { accountName: 'user2' }, | ||
87 | statusCodeExpected: 404 | ||
88 | }) | ||
89 | }) | ||
90 | |||
91 | it('Should fail to block ourselves', async function () { | ||
92 | await makePostBodyRequest({ | ||
93 | url: server.url, | ||
94 | token: server.accessToken, | ||
95 | path, | ||
96 | fields: { accountName: 'root' }, | ||
97 | statusCodeExpected: 409 | ||
98 | }) | ||
99 | }) | ||
100 | |||
101 | it('Should succeed with the correct params', async function () { | ||
102 | await makePostBodyRequest({ | ||
103 | url: server.url, | ||
104 | token: server.accessToken, | ||
105 | path, | ||
106 | fields: { accountName: 'user1' }, | ||
107 | statusCodeExpected: 204 | ||
108 | }) | ||
109 | }) | ||
110 | }) | ||
111 | |||
112 | describe('When unblocking an account', function () { | ||
113 | it('Should fail with an unauthenticated user', async function () { | ||
114 | await makeDeleteRequest({ | ||
115 | url: server.url, | ||
116 | path: path + '/user1', | ||
117 | statusCodeExpected: 401 | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | it('Should fail with an unknown account block', async function () { | ||
122 | await makeDeleteRequest({ | ||
123 | url: server.url, | ||
124 | path: path + '/user2', | ||
125 | token: server.accessToken, | ||
126 | statusCodeExpected: 404 | ||
127 | }) | ||
128 | }) | ||
129 | |||
130 | it('Should succeed with the correct params', async function () { | ||
131 | await makeDeleteRequest({ | ||
132 | url: server.url, | ||
133 | path: path + '/user1', | ||
134 | token: server.accessToken, | ||
135 | statusCodeExpected: 204 | ||
136 | }) | ||
137 | }) | ||
138 | }) | ||
139 | }) | ||
140 | |||
141 | describe('When managing user servers blocklist', function () { | ||
142 | const path = '/api/v1/users/me/blocklist/servers' | ||
143 | |||
144 | describe('When listing blocked servers', function () { | ||
145 | it('Should fail with an unauthenticated user', async function () { | ||
146 | await makeGetRequest({ | ||
147 | url: server.url, | ||
148 | path, | ||
149 | statusCodeExpected: 401 | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | it('Should fail with a bad start pagination', async function () { | ||
154 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
155 | }) | ||
156 | |||
157 | it('Should fail with a bad count pagination', async function () { | ||
158 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
159 | }) | ||
160 | |||
161 | it('Should fail with an incorrect sort', async function () { | ||
162 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
163 | }) | ||
164 | }) | ||
165 | |||
166 | describe('When blocking a server', function () { | ||
167 | it('Should fail with an unauthenticated user', async function () { | ||
168 | await makePostBodyRequest({ | ||
169 | url: server.url, | ||
170 | path, | ||
171 | fields: { host: 'localhost:9002' }, | ||
172 | statusCodeExpected: 401 | ||
173 | }) | ||
174 | }) | ||
175 | |||
176 | it('Should fail with an unknown server', async function () { | ||
177 | await makePostBodyRequest({ | ||
178 | url: server.url, | ||
179 | token: server.accessToken, | ||
180 | path, | ||
181 | fields: { host: 'localhost:9003' }, | ||
182 | statusCodeExpected: 404 | ||
183 | }) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with our own server', async function () { | ||
187 | await makePostBodyRequest({ | ||
188 | url: server.url, | ||
189 | token: server.accessToken, | ||
190 | path, | ||
191 | fields: { host: 'localhost:9001' }, | ||
192 | statusCodeExpected: 409 | ||
193 | }) | ||
194 | }) | ||
195 | |||
196 | it('Should succeed with the correct params', async function () { | ||
197 | await makePostBodyRequest({ | ||
198 | url: server.url, | ||
199 | token: server.accessToken, | ||
200 | path, | ||
201 | fields: { host: 'localhost:9002' }, | ||
202 | statusCodeExpected: 204 | ||
203 | }) | ||
204 | }) | ||
205 | }) | ||
206 | |||
207 | describe('When unblocking a server', function () { | ||
208 | it('Should fail with an unauthenticated user', async function () { | ||
209 | await makeDeleteRequest({ | ||
210 | url: server.url, | ||
211 | path: path + '/localhost:9002', | ||
212 | statusCodeExpected: 401 | ||
213 | }) | ||
214 | }) | ||
215 | |||
216 | it('Should fail with an unknown server block', async function () { | ||
217 | await makeDeleteRequest({ | ||
218 | url: server.url, | ||
219 | path: path + '/localhost:9003', | ||
220 | token: server.accessToken, | ||
221 | statusCodeExpected: 404 | ||
222 | }) | ||
223 | }) | ||
224 | |||
225 | it('Should succeed with the correct params', async function () { | ||
226 | await makeDeleteRequest({ | ||
227 | url: server.url, | ||
228 | path: path + '/localhost:9002', | ||
229 | token: server.accessToken, | ||
230 | statusCodeExpected: 204 | ||
231 | }) | ||
232 | }) | ||
233 | }) | ||
234 | }) | ||
235 | }) | ||
236 | |||
237 | describe('When managing server blocklist', function () { | ||
238 | |||
239 | describe('When managing server accounts blocklist', function () { | ||
240 | const path = '/api/v1/server/blocklist/accounts' | ||
241 | |||
242 | describe('When listing blocked accounts', function () { | ||
243 | it('Should fail with an unauthenticated user', async function () { | ||
244 | await makeGetRequest({ | ||
245 | url: server.url, | ||
246 | path, | ||
247 | statusCodeExpected: 401 | ||
248 | }) | ||
249 | }) | ||
250 | |||
251 | it('Should fail with a user without the appropriate rights', async function () { | ||
252 | await makeGetRequest({ | ||
253 | url: server.url, | ||
254 | token: userAccessToken, | ||
255 | path, | ||
256 | statusCodeExpected: 403 | ||
257 | }) | ||
258 | }) | ||
259 | |||
260 | it('Should fail with a bad start pagination', async function () { | ||
261 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
262 | }) | ||
263 | |||
264 | it('Should fail with a bad count pagination', async function () { | ||
265 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
266 | }) | ||
267 | |||
268 | it('Should fail with an incorrect sort', async function () { | ||
269 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
270 | }) | ||
271 | }) | ||
272 | |||
273 | describe('When blocking an account', function () { | ||
274 | it('Should fail with an unauthenticated user', async function () { | ||
275 | await makePostBodyRequest({ | ||
276 | url: server.url, | ||
277 | path, | ||
278 | fields: { accountName: 'user1' }, | ||
279 | statusCodeExpected: 401 | ||
280 | }) | ||
281 | }) | ||
282 | |||
283 | it('Should fail with a user without the appropriate rights', async function () { | ||
284 | await makePostBodyRequest({ | ||
285 | url: server.url, | ||
286 | token: userAccessToken, | ||
287 | path, | ||
288 | fields: { accountName: 'user1' }, | ||
289 | statusCodeExpected: 403 | ||
290 | }) | ||
291 | }) | ||
292 | |||
293 | it('Should fail with an unknown account', async function () { | ||
294 | await makePostBodyRequest({ | ||
295 | url: server.url, | ||
296 | token: server.accessToken, | ||
297 | path, | ||
298 | fields: { accountName: 'user2' }, | ||
299 | statusCodeExpected: 404 | ||
300 | }) | ||
301 | }) | ||
302 | |||
303 | it('Should fail to block ourselves', async function () { | ||
304 | await makePostBodyRequest({ | ||
305 | url: server.url, | ||
306 | token: server.accessToken, | ||
307 | path, | ||
308 | fields: { accountName: 'root' }, | ||
309 | statusCodeExpected: 409 | ||
310 | }) | ||
311 | }) | ||
312 | |||
313 | it('Should succeed with the correct params', async function () { | ||
314 | await makePostBodyRequest({ | ||
315 | url: server.url, | ||
316 | token: server.accessToken, | ||
317 | path, | ||
318 | fields: { accountName: 'user1' }, | ||
319 | statusCodeExpected: 204 | ||
320 | }) | ||
321 | }) | ||
322 | }) | ||
323 | |||
324 | describe('When unblocking an account', function () { | ||
325 | it('Should fail with an unauthenticated user', async function () { | ||
326 | await makeDeleteRequest({ | ||
327 | url: server.url, | ||
328 | path: path + '/user1', | ||
329 | statusCodeExpected: 401 | ||
330 | }) | ||
331 | }) | ||
332 | |||
333 | it('Should fail with a user without the appropriate rights', async function () { | ||
334 | await makeDeleteRequest({ | ||
335 | url: server.url, | ||
336 | path: path + '/user1', | ||
337 | token: userAccessToken, | ||
338 | statusCodeExpected: 403 | ||
339 | }) | ||
340 | }) | ||
341 | |||
342 | it('Should fail with an unknown account block', async function () { | ||
343 | await makeDeleteRequest({ | ||
344 | url: server.url, | ||
345 | path: path + '/user2', | ||
346 | token: server.accessToken, | ||
347 | statusCodeExpected: 404 | ||
348 | }) | ||
349 | }) | ||
350 | |||
351 | it('Should succeed with the correct params', async function () { | ||
352 | await makeDeleteRequest({ | ||
353 | url: server.url, | ||
354 | path: path + '/user1', | ||
355 | token: server.accessToken, | ||
356 | statusCodeExpected: 204 | ||
357 | }) | ||
358 | }) | ||
359 | }) | ||
360 | }) | ||
361 | |||
362 | describe('When managing server servers blocklist', function () { | ||
363 | const path = '/api/v1/server/blocklist/servers' | ||
364 | |||
365 | describe('When listing blocked servers', function () { | ||
366 | it('Should fail with an unauthenticated user', async function () { | ||
367 | await makeGetRequest({ | ||
368 | url: server.url, | ||
369 | path, | ||
370 | statusCodeExpected: 401 | ||
371 | }) | ||
372 | }) | ||
373 | |||
374 | it('Should fail with a user without the appropriate rights', async function () { | ||
375 | await makeGetRequest({ | ||
376 | url: server.url, | ||
377 | token: userAccessToken, | ||
378 | path, | ||
379 | statusCodeExpected: 403 | ||
380 | }) | ||
381 | }) | ||
382 | |||
383 | it('Should fail with a bad start pagination', async function () { | ||
384 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
385 | }) | ||
386 | |||
387 | it('Should fail with a bad count pagination', async function () { | ||
388 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
389 | }) | ||
390 | |||
391 | it('Should fail with an incorrect sort', async function () { | ||
392 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
393 | }) | ||
394 | }) | ||
395 | |||
396 | describe('When blocking a server', function () { | ||
397 | it('Should fail with an unauthenticated user', async function () { | ||
398 | await makePostBodyRequest({ | ||
399 | url: server.url, | ||
400 | path, | ||
401 | fields: { host: 'localhost:9002' }, | ||
402 | statusCodeExpected: 401 | ||
403 | }) | ||
404 | }) | ||
405 | |||
406 | it('Should fail with a user without the appropriate rights', async function () { | ||
407 | await makePostBodyRequest({ | ||
408 | url: server.url, | ||
409 | token: userAccessToken, | ||
410 | path, | ||
411 | fields: { host: 'localhost:9002' }, | ||
412 | statusCodeExpected: 403 | ||
413 | }) | ||
414 | }) | ||
415 | |||
416 | it('Should fail with an unknown server', async function () { | ||
417 | await makePostBodyRequest({ | ||
418 | url: server.url, | ||
419 | token: server.accessToken, | ||
420 | path, | ||
421 | fields: { host: 'localhost:9003' }, | ||
422 | statusCodeExpected: 404 | ||
423 | }) | ||
424 | }) | ||
425 | |||
426 | it('Should fail with our own server', async function () { | ||
427 | await makePostBodyRequest({ | ||
428 | url: server.url, | ||
429 | token: server.accessToken, | ||
430 | path, | ||
431 | fields: { host: 'localhost:9001' }, | ||
432 | statusCodeExpected: 409 | ||
433 | }) | ||
434 | }) | ||
435 | |||
436 | it('Should succeed with the correct params', async function () { | ||
437 | await makePostBodyRequest({ | ||
438 | url: server.url, | ||
439 | token: server.accessToken, | ||
440 | path, | ||
441 | fields: { host: 'localhost:9002' }, | ||
442 | statusCodeExpected: 204 | ||
443 | }) | ||
444 | }) | ||
445 | }) | ||
446 | |||
447 | describe('When unblocking a server', function () { | ||
448 | it('Should fail with an unauthenticated user', async function () { | ||
449 | await makeDeleteRequest({ | ||
450 | url: server.url, | ||
451 | path: path + '/localhost:9002', | ||
452 | statusCodeExpected: 401 | ||
453 | }) | ||
454 | }) | ||
455 | |||
456 | it('Should fail with a user without the appropriate rights', async function () { | ||
457 | await makeDeleteRequest({ | ||
458 | url: server.url, | ||
459 | path: path + '/localhost:9002', | ||
460 | token: userAccessToken, | ||
461 | statusCodeExpected: 403 | ||
462 | }) | ||
463 | }) | ||
464 | |||
465 | it('Should fail with an unknown server block', async function () { | ||
466 | await makeDeleteRequest({ | ||
467 | url: server.url, | ||
468 | path: path + '/localhost:9003', | ||
469 | token: server.accessToken, | ||
470 | statusCodeExpected: 404 | ||
471 | }) | ||
472 | }) | ||
473 | |||
474 | it('Should succeed with the correct params', async function () { | ||
475 | await makeDeleteRequest({ | ||
476 | url: server.url, | ||
477 | path: path + '/localhost:9002', | ||
478 | token: server.accessToken, | ||
479 | statusCodeExpected: 204 | ||
480 | }) | ||
481 | }) | ||
482 | }) | ||
483 | }) | ||
484 | }) | ||
485 | |||
486 | after(async function () { | ||
487 | killallServers(servers) | ||
488 | |||
489 | // Keep the logs if the test failed | ||
490 | if (this['ok']) { | ||
491 | await flushTests() | ||
492 | } | ||
493 | }) | ||
494 | }) | ||
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 44460a167..877ceb0a7 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './accounts' | 2 | import './accounts' |
3 | import './blocklist' | ||
3 | import './config' | 4 | import './config' |
4 | import './follows' | 5 | import './follows' |
5 | import './jobs' | 6 | import './jobs' |
@@ -15,3 +16,5 @@ import './video-channels' | |||
15 | import './video-comments' | 16 | import './video-comments' |
16 | import './video-imports' | 17 | import './video-imports' |
17 | import './videos' | 18 | import './videos' |
19 | import './videos-filter' | ||
20 | import './videos-history' | ||
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index cbfa0c137..ec46609a4 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -315,7 +315,7 @@ describe('Test users API validators', function () { | |||
315 | 315 | ||
316 | it('Should fail with a too long description', async function () { | 316 | it('Should fail with a too long description', async function () { |
317 | const fields = { | 317 | const fields = { |
318 | description: 'super'.repeat(60) | 318 | description: 'super'.repeat(201) |
319 | } | 319 | } |
320 | 320 | ||
321 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | 321 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 3a7942945..e5696224d 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -118,12 +118,12 @@ describe('Test video channels API validator', function () { | |||
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should fail with a long description', async function () { | 120 | it('Should fail with a long description', async function () { |
121 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) | 121 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) }) |
122 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | 122 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) |
123 | }) | 123 | }) |
124 | 124 | ||
125 | it('Should fail with a long support text', async function () { | 125 | it('Should fail with a long support text', async function () { |
126 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 126 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
127 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) | 127 | await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) |
128 | }) | 128 | }) |
129 | 129 | ||
@@ -185,12 +185,12 @@ describe('Test video channels API validator', function () { | |||
185 | }) | 185 | }) |
186 | 186 | ||
187 | it('Should fail with a long description', async function () { | 187 | it('Should fail with a long description', async function () { |
188 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) | 188 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) }) |
189 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 189 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
190 | }) | 190 | }) |
191 | 191 | ||
192 | it('Should fail with a long support text', async function () { | 192 | it('Should fail with a long support text', async function () { |
193 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 193 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
194 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 194 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
195 | }) | 195 | }) |
196 | 196 | ||
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 44645b0e2..b51f3d2cd 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -140,7 +140,7 @@ describe('Test video imports API validator', function () { | |||
140 | }) | 140 | }) |
141 | 141 | ||
142 | it('Should fail with a long support text', async function () { | 142 | it('Should fail with a long support text', async function () { |
143 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 143 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
144 | 144 | ||
145 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 145 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
146 | }) | 146 | }) |
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts new file mode 100644 index 000000000..784cd8ba1 --- /dev/null +++ b/server/tests/api/check-params/videos-filter.ts | |||
@@ -0,0 +1,127 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | makeGetRequest, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | userLogin | ||
14 | } from '../../utils' | ||
15 | import { UserRole } from '../../../../shared/models/users' | ||
16 | |||
17 | const expect = chai.expect | ||
18 | |||
19 | async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) { | ||
20 | const paths = [ | ||
21 | '/api/v1/video-channels/root_channel/videos', | ||
22 | '/api/v1/accounts/root/videos', | ||
23 | '/api/v1/videos', | ||
24 | '/api/v1/search/videos' | ||
25 | ] | ||
26 | |||
27 | for (const path of paths) { | ||
28 | await makeGetRequest({ | ||
29 | url: server.url, | ||
30 | path, | ||
31 | token, | ||
32 | query: { | ||
33 | filter | ||
34 | }, | ||
35 | statusCodeExpected | ||
36 | }) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | describe('Test videos filters', function () { | ||
41 | let server: ServerInfo | ||
42 | let userAccessToken: string | ||
43 | let moderatorAccessToken: string | ||
44 | |||
45 | // --------------------------------------------------------------- | ||
46 | |||
47 | before(async function () { | ||
48 | this.timeout(30000) | ||
49 | |||
50 | await flushTests() | ||
51 | |||
52 | server = await runServer(1) | ||
53 | |||
54 | await setAccessTokensToServers([ server ]) | ||
55 | |||
56 | const user = { username: 'user1', password: 'my super password' } | ||
57 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
58 | userAccessToken = await userLogin(server, user) | ||
59 | |||
60 | const moderator = { username: 'moderator', password: 'my super password' } | ||
61 | await createUser( | ||
62 | server.url, | ||
63 | server.accessToken, | ||
64 | moderator.username, | ||
65 | moderator.password, | ||
66 | undefined, | ||
67 | undefined, | ||
68 | UserRole.MODERATOR | ||
69 | ) | ||
70 | moderatorAccessToken = await userLogin(server, moderator) | ||
71 | }) | ||
72 | |||
73 | describe('When setting a video filter', function () { | ||
74 | |||
75 | it('Should fail with a bad filter', async function () { | ||
76 | await testEndpoints(server, server.accessToken, 'bad-filter', 400) | ||
77 | }) | ||
78 | |||
79 | it('Should succeed with a good filter', async function () { | ||
80 | await testEndpoints(server, server.accessToken,'local', 200) | ||
81 | }) | ||
82 | |||
83 | it('Should fail to list all-local with a simple user', async function () { | ||
84 | await testEndpoints(server, userAccessToken, 'all-local', 401) | ||
85 | }) | ||
86 | |||
87 | it('Should succeed to list all-local with a moderator', async function () { | ||
88 | await testEndpoints(server, moderatorAccessToken, 'all-local', 200) | ||
89 | }) | ||
90 | |||
91 | it('Should succeed to list all-local with an admin', async function () { | ||
92 | await testEndpoints(server, server.accessToken, 'all-local', 200) | ||
93 | }) | ||
94 | |||
95 | // Because we cannot authenticate the user on the RSS endpoint | ||
96 | it('Should fail on the feeds endpoint with the all-local filter', async function () { | ||
97 | await makeGetRequest({ | ||
98 | url: server.url, | ||
99 | path: '/feeds/videos.json', | ||
100 | statusCodeExpected: 401, | ||
101 | query: { | ||
102 | filter: 'all-local' | ||
103 | } | ||
104 | }) | ||
105 | }) | ||
106 | |||
107 | it('Should succed on the feeds endpoint with the local filter', async function () { | ||
108 | await makeGetRequest({ | ||
109 | url: server.url, | ||
110 | path: '/feeds/videos.json', | ||
111 | statusCodeExpected: 200, | ||
112 | query: { | ||
113 | filter: 'local' | ||
114 | } | ||
115 | }) | ||
116 | }) | ||
117 | }) | ||
118 | |||
119 | after(async function () { | ||
120 | killallServers([ server ]) | ||
121 | |||
122 | // Keep the logs if the test failed | ||
123 | if (this['ok']) { | ||
124 | await flushTests() | ||
125 | } | ||
126 | }) | ||
127 | }) | ||
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts new file mode 100644 index 000000000..808c3b616 --- /dev/null +++ b/server/tests/api/check-params/videos-history.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | flushTests, | ||
7 | killallServers, | ||
8 | makePostBodyRequest, | ||
9 | makePutBodyRequest, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../utils' | ||
15 | |||
16 | const expect = chai.expect | ||
17 | |||
18 | describe('Test videos history API validator', function () { | ||
19 | let path: string | ||
20 | let server: ServerInfo | ||
21 | |||
22 | // --------------------------------------------------------------- | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | await flushTests() | ||
28 | |||
29 | server = await runServer(1) | ||
30 | |||
31 | await setAccessTokensToServers([ server ]) | ||
32 | |||
33 | const res = await uploadVideo(server.url, server.accessToken, {}) | ||
34 | const videoUUID = res.body.video.uuid | ||
35 | |||
36 | path = '/api/v1/videos/' + videoUUID + '/watching' | ||
37 | }) | ||
38 | |||
39 | describe('When notifying a user is watching a video', function () { | ||
40 | |||
41 | it('Should fail with an unauthenticated user', async function () { | ||
42 | const fields = { currentTime: 5 } | ||
43 | await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 }) | ||
44 | }) | ||
45 | |||
46 | it('Should fail with an incorrect video id', async function () { | ||
47 | const fields = { currentTime: 5 } | ||
48 | const path = '/api/v1/videos/blabla/watching' | ||
49 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 }) | ||
50 | }) | ||
51 | |||
52 | it('Should fail with an unknown video', async function () { | ||
53 | const fields = { currentTime: 5 } | ||
54 | const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching' | ||
55 | |||
56 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 404 }) | ||
57 | }) | ||
58 | |||
59 | it('Should fail with a bad current time', async function () { | ||
60 | const fields = { currentTime: 'hello' } | ||
61 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 }) | ||
62 | }) | ||
63 | |||
64 | it('Should succeed with the correct parameters', async function () { | ||
65 | const fields = { currentTime: 5 } | ||
66 | |||
67 | await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 }) | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | after(async function () { | ||
72 | killallServers([ server ]) | ||
73 | |||
74 | // Keep the logs if the test failed | ||
75 | if (this['ok']) { | ||
76 | await flushTests() | ||
77 | } | ||
78 | }) | ||
79 | }) | ||
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 904d22870..699f135c7 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -233,7 +233,7 @@ describe('Test videos API validator', function () { | |||
233 | }) | 233 | }) |
234 | 234 | ||
235 | it('Should fail with a long support text', async function () { | 235 | it('Should fail with a long support text', async function () { |
236 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 236 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
237 | const attaches = baseCorrectAttaches | 237 | const attaches = baseCorrectAttaches |
238 | 238 | ||
239 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 239 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) |
@@ -482,7 +482,7 @@ describe('Test videos API validator', function () { | |||
482 | }) | 482 | }) |
483 | 483 | ||
484 | it('Should fail with a long support text', async function () { | 484 | it('Should fail with a long support text', async function () { |
485 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) | 485 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
486 | 486 | ||
487 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 487 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) |
488 | }) | 488 | }) |
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts new file mode 100644 index 000000000..8e69b95a6 --- /dev/null +++ b/server/tests/api/index-4.ts | |||
@@ -0,0 +1 @@ | |||
import './redundancy' | |||
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts index 2d996dbf9..bc140f860 100644 --- a/server/tests/api/index.ts +++ b/server/tests/api/index.ts | |||
@@ -2,3 +2,4 @@ | |||
2 | import './index-1' | 2 | import './index-1' |
3 | import './index-2' | 3 | import './index-2' |
4 | import './index-3' | 4 | import './index-3' |
5 | import './index-4' | ||
diff --git a/server/tests/api/redundancy/index.ts b/server/tests/api/redundancy/index.ts new file mode 100644 index 000000000..8e69b95a6 --- /dev/null +++ b/server/tests/api/redundancy/index.ts | |||
@@ -0,0 +1 @@ | |||
import './redundancy' | |||
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts new file mode 100644 index 000000000..1960854b6 --- /dev/null +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -0,0 +1,483 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { VideoDetails } from '../../../../shared/models/videos' | ||
6 | import { | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | getFollowingListPaginationAndSort, | ||
10 | getVideo, | ||
11 | immutableAssign, | ||
12 | killallServers, makeGetRequest, | ||
13 | root, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, unfollow, | ||
16 | uploadVideo, | ||
17 | viewVideo, | ||
18 | wait, | ||
19 | waitUntilLog, | ||
20 | checkVideoFilesWereRemoved, removeVideo | ||
21 | } from '../../utils' | ||
22 | import { waitJobs } from '../../utils/server/jobs' | ||
23 | import * as magnetUtil from 'magnet-uri' | ||
24 | import { updateRedundancy } from '../../utils/server/redundancy' | ||
25 | import { ActorFollow } from '../../../../shared/models/actors' | ||
26 | import { readdir } from 'fs-extra' | ||
27 | import { join } from 'path' | ||
28 | import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' | ||
29 | import { getStats } from '../../utils/server/stats' | ||
30 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | ||
31 | |||
32 | const expect = chai.expect | ||
33 | |||
34 | let servers: ServerInfo[] = [] | ||
35 | let video1Server2UUID: string | ||
36 | |||
37 | function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { | ||
38 | const parsed = magnetUtil.decode(file.magnetUri) | ||
39 | |||
40 | for (const ws of baseWebseeds) { | ||
41 | const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`) | ||
42 | expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined | ||
43 | } | ||
44 | |||
45 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) | ||
46 | } | ||
47 | |||
48 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | ||
49 | const config = { | ||
50 | redundancy: { | ||
51 | videos: { | ||
52 | check_interval: '5 seconds', | ||
53 | strategies: [ | ||
54 | immutableAssign({ | ||
55 | min_lifetime: '1 hour', | ||
56 | strategy: strategy, | ||
57 | size: '100KB' | ||
58 | }, additionalParams) | ||
59 | ] | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | servers = await flushAndRunMultipleServers(3, config) | ||
64 | |||
65 | // Get the access tokens | ||
66 | await setAccessTokensToServers(servers) | ||
67 | |||
68 | { | ||
69 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) | ||
70 | video1Server2UUID = res.body.video.uuid | ||
71 | |||
72 | await viewVideo(servers[ 1 ].url, video1Server2UUID) | ||
73 | } | ||
74 | |||
75 | await waitJobs(servers) | ||
76 | |||
77 | // Server 1 and server 2 follow each other | ||
78 | await doubleFollow(servers[ 0 ], servers[ 1 ]) | ||
79 | // Server 1 and server 3 follow each other | ||
80 | await doubleFollow(servers[ 0 ], servers[ 2 ]) | ||
81 | // Server 2 and server 3 follow each other | ||
82 | await doubleFollow(servers[ 1 ], servers[ 2 ]) | ||
83 | |||
84 | await waitJobs(servers) | ||
85 | } | ||
86 | |||
87 | async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
88 | if (!videoUUID) videoUUID = video1Server2UUID | ||
89 | |||
90 | const webseeds = [ | ||
91 | 'http://localhost:9002/static/webseed/' + videoUUID | ||
92 | ] | ||
93 | |||
94 | for (const server of servers) { | ||
95 | { | ||
96 | const res = await getVideo(server.url, videoUUID) | ||
97 | |||
98 | const video: VideoDetails = res.body | ||
99 | for (const f of video.files) { | ||
100 | checkMagnetWebseeds(f, webseeds, server) | ||
101 | } | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | |||
106 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | ||
107 | const res = await getStats(servers[0].url) | ||
108 | const data: ServerStats = res.body | ||
109 | |||
110 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
111 | const stat = data.videosRedundancy[0] | ||
112 | |||
113 | expect(stat.strategy).to.equal(strategy) | ||
114 | expect(stat.totalSize).to.equal(102400) | ||
115 | expect(stat.totalUsed).to.be.at.least(1).and.below(102401) | ||
116 | expect(stat.totalVideoFiles).to.equal(4) | ||
117 | expect(stat.totalVideos).to.equal(1) | ||
118 | } | ||
119 | |||
120 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
121 | const res = await getStats(servers[0].url) | ||
122 | const data: ServerStats = res.body | ||
123 | |||
124 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
125 | |||
126 | const stat = data.videosRedundancy[0] | ||
127 | expect(stat.strategy).to.equal(strategy) | ||
128 | expect(stat.totalSize).to.equal(102400) | ||
129 | expect(stat.totalUsed).to.equal(0) | ||
130 | expect(stat.totalVideoFiles).to.equal(0) | ||
131 | expect(stat.totalVideos).to.equal(0) | ||
132 | } | ||
133 | |||
134 | async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
135 | if (!videoUUID) videoUUID = video1Server2UUID | ||
136 | |||
137 | const webseeds = [ | ||
138 | 'http://localhost:9001/static/webseed/' + videoUUID, | ||
139 | 'http://localhost:9002/static/webseed/' + videoUUID | ||
140 | ] | ||
141 | |||
142 | for (const server of servers) { | ||
143 | const res = await getVideo(server.url, videoUUID) | ||
144 | |||
145 | const video: VideoDetails = res.body | ||
146 | |||
147 | for (const file of video.files) { | ||
148 | checkMagnetWebseeds(file, webseeds, server) | ||
149 | |||
150 | // Only servers 1 and 2 have the video | ||
151 | if (server.serverNumber !== 3) { | ||
152 | await makeGetRequest({ | ||
153 | url: server.url, | ||
154 | statusCodeExpected: 200, | ||
155 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | ||
156 | contentType: null | ||
157 | }) | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | for (const directory of [ 'test1', 'test2' ]) { | ||
163 | const files = await readdir(join(root(), directory, 'videos')) | ||
164 | expect(files).to.have.length.at.least(4) | ||
165 | |||
166 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
167 | expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined | ||
168 | } | ||
169 | } | ||
170 | } | ||
171 | |||
172 | async function enableRedundancyOnServer1 () { | ||
173 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | ||
174 | |||
175 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') | ||
176 | const follows: ActorFollow[] = res.body.data | ||
177 | const server2 = follows.find(f => f.following.host === 'localhost:9002') | ||
178 | const server3 = follows.find(f => f.following.host === 'localhost:9003') | ||
179 | |||
180 | expect(server3).to.not.be.undefined | ||
181 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
182 | |||
183 | expect(server2).to.not.be.undefined | ||
184 | expect(server2.following.hostRedundancyAllowed).to.be.true | ||
185 | } | ||
186 | |||
187 | async function disableRedundancyOnServer1 () { | ||
188 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false) | ||
189 | |||
190 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') | ||
191 | const follows: ActorFollow[] = res.body.data | ||
192 | const server2 = follows.find(f => f.following.host === 'localhost:9002') | ||
193 | const server3 = follows.find(f => f.following.host === 'localhost:9003') | ||
194 | |||
195 | expect(server3).to.not.be.undefined | ||
196 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
197 | |||
198 | expect(server2).to.not.be.undefined | ||
199 | expect(server2.following.hostRedundancyAllowed).to.be.false | ||
200 | } | ||
201 | |||
202 | async function cleanServers () { | ||
203 | killallServers(servers) | ||
204 | } | ||
205 | |||
206 | describe('Test videos redundancy', function () { | ||
207 | |||
208 | describe('With most-views strategy', function () { | ||
209 | const strategy = 'most-views' | ||
210 | |||
211 | before(function () { | ||
212 | this.timeout(120000) | ||
213 | |||
214 | return runServers(strategy) | ||
215 | }) | ||
216 | |||
217 | it('Should have 1 webseed on the first video', async function () { | ||
218 | await check1WebSeed(strategy) | ||
219 | await checkStatsWith1Webseed(strategy) | ||
220 | }) | ||
221 | |||
222 | it('Should enable redundancy on server 1', function () { | ||
223 | return enableRedundancyOnServer1() | ||
224 | }) | ||
225 | |||
226 | it('Should have 2 webseed on the first video', async function () { | ||
227 | this.timeout(40000) | ||
228 | |||
229 | await waitJobs(servers) | ||
230 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
231 | await waitJobs(servers) | ||
232 | |||
233 | await check2Webseeds(strategy) | ||
234 | await checkStatsWith2Webseed(strategy) | ||
235 | }) | ||
236 | |||
237 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | ||
238 | this.timeout(40000) | ||
239 | |||
240 | await disableRedundancyOnServer1() | ||
241 | |||
242 | await waitJobs(servers) | ||
243 | await wait(5000) | ||
244 | |||
245 | await check1WebSeed(strategy) | ||
246 | |||
247 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | ||
248 | }) | ||
249 | |||
250 | after(function () { | ||
251 | return cleanServers() | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | describe('With trending strategy', function () { | ||
256 | const strategy = 'trending' | ||
257 | |||
258 | before(function () { | ||
259 | this.timeout(120000) | ||
260 | |||
261 | return runServers(strategy) | ||
262 | }) | ||
263 | |||
264 | it('Should have 1 webseed on the first video', async function () { | ||
265 | await check1WebSeed(strategy) | ||
266 | await checkStatsWith1Webseed(strategy) | ||
267 | }) | ||
268 | |||
269 | it('Should enable redundancy on server 1', function () { | ||
270 | return enableRedundancyOnServer1() | ||
271 | }) | ||
272 | |||
273 | it('Should have 2 webseed on the first video', async function () { | ||
274 | this.timeout(40000) | ||
275 | |||
276 | await waitJobs(servers) | ||
277 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
278 | await waitJobs(servers) | ||
279 | |||
280 | await check2Webseeds(strategy) | ||
281 | await checkStatsWith2Webseed(strategy) | ||
282 | }) | ||
283 | |||
284 | it('Should unfollow on server 1 and remove duplicated videos', async function () { | ||
285 | this.timeout(40000) | ||
286 | |||
287 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | ||
288 | |||
289 | await waitJobs(servers) | ||
290 | await wait(5000) | ||
291 | |||
292 | await check1WebSeed(strategy) | ||
293 | |||
294 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | ||
295 | }) | ||
296 | |||
297 | after(function () { | ||
298 | return cleanServers() | ||
299 | }) | ||
300 | }) | ||
301 | |||
302 | describe('With recently added strategy', function () { | ||
303 | const strategy = 'recently-added' | ||
304 | |||
305 | before(function () { | ||
306 | this.timeout(120000) | ||
307 | |||
308 | return runServers(strategy, { min_views: 3 }) | ||
309 | }) | ||
310 | |||
311 | it('Should have 1 webseed on the first video', async function () { | ||
312 | await check1WebSeed(strategy) | ||
313 | await checkStatsWith1Webseed(strategy) | ||
314 | }) | ||
315 | |||
316 | it('Should enable redundancy on server 1', function () { | ||
317 | return enableRedundancyOnServer1() | ||
318 | }) | ||
319 | |||
320 | it('Should still have 1 webseed on the first video', async function () { | ||
321 | this.timeout(40000) | ||
322 | |||
323 | await waitJobs(servers) | ||
324 | await wait(15000) | ||
325 | await waitJobs(servers) | ||
326 | |||
327 | await check1WebSeed(strategy) | ||
328 | await checkStatsWith1Webseed(strategy) | ||
329 | }) | ||
330 | |||
331 | it('Should view 2 times the first video to have > min_views config', async function () { | ||
332 | this.timeout(40000) | ||
333 | |||
334 | await viewVideo(servers[ 0 ].url, video1Server2UUID) | ||
335 | await viewVideo(servers[ 2 ].url, video1Server2UUID) | ||
336 | |||
337 | await wait(10000) | ||
338 | await waitJobs(servers) | ||
339 | }) | ||
340 | |||
341 | it('Should have 2 webseed on the first video', async function () { | ||
342 | this.timeout(40000) | ||
343 | |||
344 | await waitJobs(servers) | ||
345 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
346 | await waitJobs(servers) | ||
347 | |||
348 | await check2Webseeds(strategy) | ||
349 | await checkStatsWith2Webseed(strategy) | ||
350 | }) | ||
351 | |||
352 | it('Should remove the video and the redundancy files', async function () { | ||
353 | this.timeout(20000) | ||
354 | |||
355 | await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID) | ||
356 | |||
357 | await waitJobs(servers) | ||
358 | |||
359 | for (const server of servers) { | ||
360 | await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber) | ||
361 | } | ||
362 | }) | ||
363 | |||
364 | after(function () { | ||
365 | return cleanServers() | ||
366 | }) | ||
367 | }) | ||
368 | |||
369 | describe('Test expiration', function () { | ||
370 | const strategy = 'recently-added' | ||
371 | |||
372 | async function checkContains (servers: ServerInfo[], str: string) { | ||
373 | for (const server of servers) { | ||
374 | const res = await getVideo(server.url, video1Server2UUID) | ||
375 | const video: VideoDetails = res.body | ||
376 | |||
377 | for (const f of video.files) { | ||
378 | expect(f.magnetUri).to.contain(str) | ||
379 | } | ||
380 | } | ||
381 | } | ||
382 | |||
383 | async function checkNotContains (servers: ServerInfo[], str: string) { | ||
384 | for (const server of servers) { | ||
385 | const res = await getVideo(server.url, video1Server2UUID) | ||
386 | const video: VideoDetails = res.body | ||
387 | |||
388 | for (const f of video.files) { | ||
389 | expect(f.magnetUri).to.not.contain(str) | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | |||
394 | before(async function () { | ||
395 | this.timeout(120000) | ||
396 | |||
397 | await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
398 | |||
399 | await enableRedundancyOnServer1() | ||
400 | }) | ||
401 | |||
402 | it('Should still have 2 webseeds after 10 seconds', async function () { | ||
403 | this.timeout(40000) | ||
404 | |||
405 | await wait(10000) | ||
406 | |||
407 | try { | ||
408 | await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') | ||
409 | } catch { | ||
410 | // Maybe a server deleted a redundancy in the scheduler | ||
411 | await wait(2000) | ||
412 | |||
413 | await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') | ||
414 | } | ||
415 | }) | ||
416 | |||
417 | it('Should stop server 1 and expire video redundancy', async function () { | ||
418 | this.timeout(40000) | ||
419 | |||
420 | killallServers([ servers[0] ]) | ||
421 | |||
422 | await wait(10000) | ||
423 | |||
424 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') | ||
425 | }) | ||
426 | |||
427 | after(function () { | ||
428 | return killallServers([ servers[1], servers[2] ]) | ||
429 | }) | ||
430 | }) | ||
431 | |||
432 | describe('Test file replacement', function () { | ||
433 | let video2Server2UUID: string | ||
434 | const strategy = 'recently-added' | ||
435 | |||
436 | before(async function () { | ||
437 | this.timeout(120000) | ||
438 | |||
439 | await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
440 | |||
441 | await enableRedundancyOnServer1() | ||
442 | |||
443 | await waitJobs(servers) | ||
444 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
445 | await waitJobs(servers) | ||
446 | |||
447 | await check2Webseeds(strategy) | ||
448 | await checkStatsWith2Webseed(strategy) | ||
449 | |||
450 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | ||
451 | video2Server2UUID = res.body.video.uuid | ||
452 | }) | ||
453 | |||
454 | it('Should cache video 2 webseed on the first video', async function () { | ||
455 | this.timeout(50000) | ||
456 | |||
457 | await waitJobs(servers) | ||
458 | |||
459 | await wait(7000) | ||
460 | |||
461 | try { | ||
462 | await check1WebSeed(strategy, video1Server2UUID) | ||
463 | await check2Webseeds(strategy, video2Server2UUID) | ||
464 | } catch { | ||
465 | await wait(3000) | ||
466 | |||
467 | try { | ||
468 | await check1WebSeed(strategy, video1Server2UUID) | ||
469 | await check2Webseeds(strategy, video2Server2UUID) | ||
470 | } catch { | ||
471 | await wait(5000) | ||
472 | |||
473 | await check1WebSeed(strategy, video1Server2UUID) | ||
474 | await check2Webseeds(strategy, video2Server2UUID) | ||
475 | } | ||
476 | } | ||
477 | }) | ||
478 | |||
479 | after(function () { | ||
480 | return cleanServers() | ||
481 | }) | ||
482 | }) | ||
483 | }) | ||
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 310c291bf..e80e93e7f 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -93,7 +93,26 @@ describe('Test follows', function () { | |||
93 | expect(server3Follow.state).to.equal('accepted') | 93 | expect(server3Follow.state).to.equal('accepted') |
94 | }) | 94 | }) |
95 | 95 | ||
96 | it('Should have 0 followings on server 1 and 2', async function () { | 96 | it('Should search followings on server 1', async function () { |
97 | { | ||
98 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':9002') | ||
99 | const follows = res.body.data | ||
100 | |||
101 | expect(res.body.total).to.equal(1) | ||
102 | expect(follows.length).to.equal(1) | ||
103 | expect(follows[ 0 ].following.host).to.equal('localhost:9002') | ||
104 | } | ||
105 | |||
106 | { | ||
107 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', 'bla') | ||
108 | const follows = res.body.data | ||
109 | |||
110 | expect(res.body.total).to.equal(0) | ||
111 | expect(follows.length).to.equal(0) | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | it('Should have 0 followings on server 2 and 3', async function () { | ||
97 | for (const server of [ servers[1], servers[2] ]) { | 116 | for (const server of [ servers[1], servers[2] ]) { |
98 | const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') | 117 | const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') |
99 | const follows = res.body.data | 118 | const follows = res.body.data |
@@ -116,6 +135,25 @@ describe('Test follows', function () { | |||
116 | } | 135 | } |
117 | }) | 136 | }) |
118 | 137 | ||
138 | it('Should search followers on server 2', async function () { | ||
139 | { | ||
140 | const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', '9001') | ||
141 | const follows = res.body.data | ||
142 | |||
143 | expect(res.body.total).to.equal(1) | ||
144 | expect(follows.length).to.equal(1) | ||
145 | expect(follows[ 0 ].following.host).to.equal('localhost:9003') | ||
146 | } | ||
147 | |||
148 | { | ||
149 | const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', 'bla') | ||
150 | const follows = res.body.data | ||
151 | |||
152 | expect(res.body.total).to.equal(0) | ||
153 | expect(follows.length).to.equal(0) | ||
154 | } | ||
155 | }) | ||
156 | |||
119 | it('Should have 0 followers on server 1', async function () { | 157 | it('Should have 0 followers on server 1', async function () { |
120 | const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') | 158 | const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') |
121 | const follows = res.body.data | 159 | const follows = res.body.data |
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index c74c68a33..eeb8b7a28 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -3,7 +3,6 @@ import './email' | |||
3 | import './follows' | 3 | import './follows' |
4 | import './handle-down' | 4 | import './handle-down' |
5 | import './jobs' | 5 | import './jobs' |
6 | import './redundancy' | ||
7 | import './reverse-proxy' | 6 | import './reverse-proxy' |
8 | import './stats' | 7 | import './stats' |
9 | import './tracker' | 8 | import './tracker' |
diff --git a/server/tests/api/server/redundancy.ts b/server/tests/api/server/redundancy.ts index 1960854b6..f50d6e3cf 100644 --- a/server/tests/api/server/redundancy.ts +++ b/server/tests/api/server/redundancy.ts | |||
@@ -419,7 +419,7 @@ describe('Test videos redundancy', function () { | |||
419 | 419 | ||
420 | killallServers([ servers[0] ]) | 420 | killallServers([ servers[0] ]) |
421 | 421 | ||
422 | await wait(10000) | 422 | await wait(15000) |
423 | 423 | ||
424 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') | 424 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') |
425 | }) | 425 | }) |
@@ -452,26 +452,22 @@ describe('Test videos redundancy', function () { | |||
452 | }) | 452 | }) |
453 | 453 | ||
454 | it('Should cache video 2 webseed on the first video', async function () { | 454 | it('Should cache video 2 webseed on the first video', async function () { |
455 | this.timeout(50000) | 455 | this.timeout(120000) |
456 | 456 | ||
457 | await waitJobs(servers) | 457 | await waitJobs(servers) |
458 | 458 | ||
459 | await wait(7000) | 459 | let checked = false |
460 | 460 | ||
461 | try { | 461 | while (checked === false) { |
462 | await check1WebSeed(strategy, video1Server2UUID) | 462 | await wait(1000) |
463 | await check2Webseeds(strategy, video2Server2UUID) | ||
464 | } catch { | ||
465 | await wait(3000) | ||
466 | 463 | ||
467 | try { | 464 | try { |
468 | await check1WebSeed(strategy, video1Server2UUID) | 465 | await check1WebSeed(strategy, video1Server2UUID) |
469 | await check2Webseeds(strategy, video2Server2UUID) | 466 | await check2Webseeds(strategy, video2Server2UUID) |
470 | } catch { | ||
471 | await wait(5000) | ||
472 | 467 | ||
473 | await check1WebSeed(strategy, video1Server2UUID) | 468 | checked = true |
474 | await check2Webseeds(strategy, video2Server2UUID) | 469 | } catch { |
470 | checked = false | ||
475 | } | 471 | } |
476 | } | 472 | } |
477 | }) | 473 | }) |
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts new file mode 100644 index 000000000..eed4b9f3e --- /dev/null +++ b/server/tests/api/users/blocklist.ts | |||
@@ -0,0 +1,511 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' | ||
6 | import { | ||
7 | createUser, | ||
8 | doubleFollow, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, | ||
11 | killallServers, | ||
12 | ServerInfo, | ||
13 | uploadVideo, | ||
14 | userLogin | ||
15 | } from '../../utils/index' | ||
16 | import { setAccessTokensToServers } from '../../utils/users/login' | ||
17 | import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos' | ||
18 | import { | ||
19 | addVideoCommentReply, | ||
20 | addVideoCommentThread, | ||
21 | getVideoCommentThreads, | ||
22 | getVideoThreadComments | ||
23 | } from '../../utils/videos/video-comments' | ||
24 | import { waitJobs } from '../../utils/server/jobs' | ||
25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
26 | import { | ||
27 | addAccountToAccountBlocklist, | ||
28 | addAccountToServerBlocklist, | ||
29 | addServerToAccountBlocklist, | ||
30 | addServerToServerBlocklist, | ||
31 | getAccountBlocklistByAccount, | ||
32 | getAccountBlocklistByServer, | ||
33 | getServerBlocklistByAccount, | ||
34 | getServerBlocklistByServer, | ||
35 | removeAccountFromAccountBlocklist, | ||
36 | removeAccountFromServerBlocklist, | ||
37 | removeServerFromAccountBlocklist, | ||
38 | removeServerFromServerBlocklist | ||
39 | } from '../../utils/users/blocklist' | ||
40 | |||
41 | const expect = chai.expect | ||
42 | |||
43 | async function checkAllVideos (url: string, token: string) { | ||
44 | { | ||
45 | const res = await getVideosListWithToken(url, token) | ||
46 | |||
47 | expect(res.body.data).to.have.lengthOf(4) | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const res = await getVideosList(url) | ||
52 | |||
53 | expect(res.body.data).to.have.lengthOf(4) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | async function checkAllComments (url: string, token: string, videoUUID: string) { | ||
58 | const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 5, '-createdAt', token) | ||
59 | |||
60 | const threads: VideoComment[] = resThreads.body.data | ||
61 | expect(threads).to.have.lengthOf(2) | ||
62 | |||
63 | for (const thread of threads) { | ||
64 | const res = await getVideoThreadComments(url, videoUUID, thread.id, token) | ||
65 | |||
66 | const tree: VideoCommentThreadTree = res.body | ||
67 | expect(tree.children).to.have.lengthOf(1) | ||
68 | } | ||
69 | } | ||
70 | |||
71 | describe('Test blocklist', function () { | ||
72 | let servers: ServerInfo[] | ||
73 | let videoUUID1: string | ||
74 | let videoUUID2: string | ||
75 | let userToken1: string | ||
76 | let userModeratorToken: string | ||
77 | let userToken2: string | ||
78 | |||
79 | before(async function () { | ||
80 | this.timeout(60000) | ||
81 | |||
82 | await flushTests() | ||
83 | |||
84 | servers = await flushAndRunMultipleServers(2) | ||
85 | await setAccessTokensToServers(servers) | ||
86 | |||
87 | { | ||
88 | const user = { username: 'user1', password: 'password' } | ||
89 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | ||
90 | |||
91 | userToken1 = await userLogin(servers[0], user) | ||
92 | await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' }) | ||
93 | } | ||
94 | |||
95 | { | ||
96 | const user = { username: 'moderator', password: 'password' } | ||
97 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | ||
98 | |||
99 | userModeratorToken = await userLogin(servers[0], user) | ||
100 | } | ||
101 | |||
102 | { | ||
103 | const user = { username: 'user2', password: 'password' } | ||
104 | await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) | ||
105 | |||
106 | userToken2 = await userLogin(servers[1], user) | ||
107 | await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' }) | ||
108 | } | ||
109 | |||
110 | { | ||
111 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' }) | ||
112 | videoUUID1 = res.body.video.uuid | ||
113 | } | ||
114 | |||
115 | { | ||
116 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' }) | ||
117 | videoUUID2 = res.body.video.uuid | ||
118 | } | ||
119 | |||
120 | await doubleFollow(servers[0], servers[1]) | ||
121 | |||
122 | { | ||
123 | const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1') | ||
124 | const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1') | ||
125 | await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1') | ||
126 | } | ||
127 | |||
128 | { | ||
129 | const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1') | ||
130 | await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1') | ||
131 | } | ||
132 | |||
133 | await waitJobs(servers) | ||
134 | }) | ||
135 | |||
136 | describe('User blocklist', function () { | ||
137 | |||
138 | describe('When managing account blocklist', function () { | ||
139 | it('Should list all videos', function () { | ||
140 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
141 | }) | ||
142 | |||
143 | it('Should list the comments', function () { | ||
144 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
145 | }) | ||
146 | |||
147 | it('Should block a remote account', async function () { | ||
148 | await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
149 | }) | ||
150 | |||
151 | it('Should hide its videos', async function () { | ||
152 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
153 | |||
154 | const videos: Video[] = res.body.data | ||
155 | expect(videos).to.have.lengthOf(3) | ||
156 | |||
157 | const v = videos.find(v => v.name === 'video user 2') | ||
158 | expect(v).to.be.undefined | ||
159 | }) | ||
160 | |||
161 | it('Should block a local account', async function () { | ||
162 | await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
163 | }) | ||
164 | |||
165 | it('Should hide its videos', async function () { | ||
166 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
167 | |||
168 | const videos: Video[] = res.body.data | ||
169 | expect(videos).to.have.lengthOf(2) | ||
170 | |||
171 | const v = videos.find(v => v.name === 'video user 1') | ||
172 | expect(v).to.be.undefined | ||
173 | }) | ||
174 | |||
175 | it('Should hide its comments', async function () { | ||
176 | const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', servers[ 0 ].accessToken) | ||
177 | |||
178 | const threads: VideoComment[] = resThreads.body.data | ||
179 | expect(threads).to.have.lengthOf(1) | ||
180 | expect(threads[ 0 ].totalReplies).to.equal(0) | ||
181 | |||
182 | const t = threads.find(t => t.text === 'comment user 1') | ||
183 | expect(t).to.be.undefined | ||
184 | |||
185 | for (const thread of threads) { | ||
186 | const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, servers[ 0 ].accessToken) | ||
187 | |||
188 | const tree: VideoCommentThreadTree = res.body | ||
189 | expect(tree.children).to.have.lengthOf(0) | ||
190 | } | ||
191 | }) | ||
192 | |||
193 | it('Should list all the videos with another user', async function () { | ||
194 | return checkAllVideos(servers[ 0 ].url, userToken1) | ||
195 | }) | ||
196 | |||
197 | it('Should list all the comments with another user', async function () { | ||
198 | return checkAllComments(servers[ 0 ].url, userToken1, videoUUID1) | ||
199 | }) | ||
200 | |||
201 | it('Should list blocked accounts', async function () { | ||
202 | { | ||
203 | const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
204 | const blocks: AccountBlock[] = res.body.data | ||
205 | |||
206 | expect(res.body.total).to.equal(2) | ||
207 | |||
208 | const block = blocks[ 0 ] | ||
209 | expect(block.byAccount.displayName).to.equal('root') | ||
210 | expect(block.byAccount.name).to.equal('root') | ||
211 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
212 | expect(block.blockedAccount.name).to.equal('user2') | ||
213 | expect(block.blockedAccount.host).to.equal('localhost:9002') | ||
214 | } | ||
215 | |||
216 | { | ||
217 | const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') | ||
218 | const blocks: AccountBlock[] = res.body.data | ||
219 | |||
220 | expect(res.body.total).to.equal(2) | ||
221 | |||
222 | const block = blocks[ 0 ] | ||
223 | expect(block.byAccount.displayName).to.equal('root') | ||
224 | expect(block.byAccount.name).to.equal('root') | ||
225 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
226 | expect(block.blockedAccount.name).to.equal('user1') | ||
227 | expect(block.blockedAccount.host).to.equal('localhost:9001') | ||
228 | } | ||
229 | }) | ||
230 | |||
231 | it('Should unblock the remote account', async function () { | ||
232 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
233 | }) | ||
234 | |||
235 | it('Should display its videos', async function () { | ||
236 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
237 | |||
238 | const videos: Video[] = res.body.data | ||
239 | expect(videos).to.have.lengthOf(3) | ||
240 | |||
241 | const v = videos.find(v => v.name === 'video user 2') | ||
242 | expect(v).not.to.be.undefined | ||
243 | }) | ||
244 | |||
245 | it('Should unblock the local account', async function () { | ||
246 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
247 | }) | ||
248 | |||
249 | it('Should display its comments', function () { | ||
250 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
251 | }) | ||
252 | }) | ||
253 | |||
254 | describe('When managing server blocklist', function () { | ||
255 | it('Should list all videos', function () { | ||
256 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
257 | }) | ||
258 | |||
259 | it('Should list the comments', function () { | ||
260 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
261 | }) | ||
262 | |||
263 | it('Should block a remote server', async function () { | ||
264 | await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
265 | }) | ||
266 | |||
267 | it('Should hide its videos', async function () { | ||
268 | const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
269 | |||
270 | const videos: Video[] = res.body.data | ||
271 | expect(videos).to.have.lengthOf(2) | ||
272 | |||
273 | const v1 = videos.find(v => v.name === 'video user 2') | ||
274 | const v2 = videos.find(v => v.name === 'video server 2') | ||
275 | |||
276 | expect(v1).to.be.undefined | ||
277 | expect(v2).to.be.undefined | ||
278 | }) | ||
279 | |||
280 | it('Should list all the videos with another user', async function () { | ||
281 | return checkAllVideos(servers[ 0 ].url, userToken1) | ||
282 | }) | ||
283 | |||
284 | it('Should hide its comments') | ||
285 | |||
286 | it('Should list blocked servers', async function () { | ||
287 | const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
288 | const blocks: ServerBlock[] = res.body.data | ||
289 | |||
290 | expect(res.body.total).to.equal(1) | ||
291 | |||
292 | const block = blocks[ 0 ] | ||
293 | expect(block.byAccount.displayName).to.equal('root') | ||
294 | expect(block.byAccount.name).to.equal('root') | ||
295 | expect(block.blockedServer.host).to.equal('localhost:9002') | ||
296 | }) | ||
297 | |||
298 | it('Should unblock the remote server', async function () { | ||
299 | await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
300 | }) | ||
301 | |||
302 | it('Should display its videos', function () { | ||
303 | return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) | ||
304 | }) | ||
305 | |||
306 | it('Should display its comments', function () { | ||
307 | return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) | ||
308 | }) | ||
309 | }) | ||
310 | }) | ||
311 | |||
312 | describe('Server blocklist', function () { | ||
313 | |||
314 | describe('When managing account blocklist', function () { | ||
315 | it('Should list all videos', async function () { | ||
316 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
317 | await checkAllVideos(servers[ 0 ].url, token) | ||
318 | } | ||
319 | }) | ||
320 | |||
321 | it('Should list the comments', async function () { | ||
322 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
323 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
324 | } | ||
325 | }) | ||
326 | |||
327 | it('Should block a remote account', async function () { | ||
328 | await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
329 | }) | ||
330 | |||
331 | it('Should hide its videos', async function () { | ||
332 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
333 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
334 | |||
335 | const videos: Video[] = res.body.data | ||
336 | expect(videos).to.have.lengthOf(3) | ||
337 | |||
338 | const v = videos.find(v => v.name === 'video user 2') | ||
339 | expect(v).to.be.undefined | ||
340 | } | ||
341 | }) | ||
342 | |||
343 | it('Should block a local account', async function () { | ||
344 | await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
345 | }) | ||
346 | |||
347 | it('Should hide its videos', async function () { | ||
348 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
349 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
350 | |||
351 | const videos: Video[] = res.body.data | ||
352 | expect(videos).to.have.lengthOf(2) | ||
353 | |||
354 | const v = videos.find(v => v.name === 'video user 1') | ||
355 | expect(v).to.be.undefined | ||
356 | } | ||
357 | }) | ||
358 | |||
359 | it('Should hide its comments', async function () { | ||
360 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
361 | const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', token) | ||
362 | |||
363 | const threads: VideoComment[] = resThreads.body.data | ||
364 | expect(threads).to.have.lengthOf(1) | ||
365 | expect(threads[ 0 ].totalReplies).to.equal(0) | ||
366 | |||
367 | const t = threads.find(t => t.text === 'comment user 1') | ||
368 | expect(t).to.be.undefined | ||
369 | |||
370 | for (const thread of threads) { | ||
371 | const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, token) | ||
372 | |||
373 | const tree: VideoCommentThreadTree = res.body | ||
374 | expect(tree.children).to.have.lengthOf(0) | ||
375 | } | ||
376 | } | ||
377 | }) | ||
378 | |||
379 | it('Should list blocked accounts', async function () { | ||
380 | { | ||
381 | const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
382 | const blocks: AccountBlock[] = res.body.data | ||
383 | |||
384 | expect(res.body.total).to.equal(2) | ||
385 | |||
386 | const block = blocks[ 0 ] | ||
387 | expect(block.byAccount.displayName).to.equal('peertube') | ||
388 | expect(block.byAccount.name).to.equal('peertube') | ||
389 | expect(block.blockedAccount.displayName).to.equal('user2') | ||
390 | expect(block.blockedAccount.name).to.equal('user2') | ||
391 | expect(block.blockedAccount.host).to.equal('localhost:9002') | ||
392 | } | ||
393 | |||
394 | { | ||
395 | const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') | ||
396 | const blocks: AccountBlock[] = res.body.data | ||
397 | |||
398 | expect(res.body.total).to.equal(2) | ||
399 | |||
400 | const block = blocks[ 0 ] | ||
401 | expect(block.byAccount.displayName).to.equal('peertube') | ||
402 | expect(block.byAccount.name).to.equal('peertube') | ||
403 | expect(block.blockedAccount.displayName).to.equal('user1') | ||
404 | expect(block.blockedAccount.name).to.equal('user1') | ||
405 | expect(block.blockedAccount.host).to.equal('localhost:9001') | ||
406 | } | ||
407 | }) | ||
408 | |||
409 | it('Should unblock the remote account', async function () { | ||
410 | await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') | ||
411 | }) | ||
412 | |||
413 | it('Should display its videos', async function () { | ||
414 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
415 | const res = await getVideosListWithToken(servers[ 0 ].url, token) | ||
416 | |||
417 | const videos: Video[] = res.body.data | ||
418 | expect(videos).to.have.lengthOf(3) | ||
419 | |||
420 | const v = videos.find(v => v.name === 'video user 2') | ||
421 | expect(v).not.to.be.undefined | ||
422 | } | ||
423 | }) | ||
424 | |||
425 | it('Should unblock the local account', async function () { | ||
426 | await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') | ||
427 | }) | ||
428 | |||
429 | it('Should display its comments', async function () { | ||
430 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
431 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
432 | } | ||
433 | }) | ||
434 | }) | ||
435 | |||
436 | describe('When managing server blocklist', function () { | ||
437 | it('Should list all videos', async function () { | ||
438 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
439 | await checkAllVideos(servers[ 0 ].url, token) | ||
440 | } | ||
441 | }) | ||
442 | |||
443 | it('Should list the comments', async function () { | ||
444 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
445 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
446 | } | ||
447 | }) | ||
448 | |||
449 | it('Should block a remote server', async function () { | ||
450 | await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
451 | }) | ||
452 | |||
453 | it('Should hide its videos', async function () { | ||
454 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
455 | const res1 = await getVideosList(servers[ 0 ].url) | ||
456 | const res2 = await getVideosListWithToken(servers[ 0 ].url, token) | ||
457 | |||
458 | for (const res of [ res1, res2 ]) { | ||
459 | const videos: Video[] = res.body.data | ||
460 | expect(videos).to.have.lengthOf(2) | ||
461 | |||
462 | const v1 = videos.find(v => v.name === 'video user 2') | ||
463 | const v2 = videos.find(v => v.name === 'video server 2') | ||
464 | |||
465 | expect(v1).to.be.undefined | ||
466 | expect(v2).to.be.undefined | ||
467 | } | ||
468 | } | ||
469 | }) | ||
470 | |||
471 | it('Should hide its comments') | ||
472 | |||
473 | it('Should list blocked servers', async function () { | ||
474 | const res = await getServerBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') | ||
475 | const blocks: ServerBlock[] = res.body.data | ||
476 | |||
477 | expect(res.body.total).to.equal(1) | ||
478 | |||
479 | const block = blocks[ 0 ] | ||
480 | expect(block.byAccount.displayName).to.equal('peertube') | ||
481 | expect(block.byAccount.name).to.equal('peertube') | ||
482 | expect(block.blockedServer.host).to.equal('localhost:9002') | ||
483 | }) | ||
484 | |||
485 | it('Should unblock the remote server', async function () { | ||
486 | await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') | ||
487 | }) | ||
488 | |||
489 | it('Should list all videos', async function () { | ||
490 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
491 | await checkAllVideos(servers[ 0 ].url, token) | ||
492 | } | ||
493 | }) | ||
494 | |||
495 | it('Should list the comments', async function () { | ||
496 | for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { | ||
497 | await checkAllComments(servers[ 0 ].url, token, videoUUID1) | ||
498 | } | ||
499 | }) | ||
500 | }) | ||
501 | }) | ||
502 | |||
503 | after(async function () { | ||
504 | killallServers(servers) | ||
505 | |||
506 | // Keep the logs if the test failed | ||
507 | if (this[ 'ok' ]) { | ||
508 | await flushTests() | ||
509 | } | ||
510 | }) | ||
511 | }) | ||
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 21d75da3e..0a1b8b0b2 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './blocklist' | ||
1 | import './user-subscriptions' | 2 | import './user-subscriptions' |
2 | import './users' | 3 | import './users' |
3 | import './users-verification' | 4 | import './users-verification' |
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts index b67072851..d8699db17 100644 --- a/server/tests/api/users/users-multiple-servers.ts +++ b/server/tests/api/users/users-multiple-servers.ts | |||
@@ -148,6 +148,12 @@ describe('Test users with multiple servers', function () { | |||
148 | expect(rootServer1Get.displayName).to.equal('my super display name') | 148 | expect(rootServer1Get.displayName).to.equal('my super display name') |
149 | expect(rootServer1Get.description).to.equal('my super description updated') | 149 | expect(rootServer1Get.description).to.equal('my super description updated') |
150 | 150 | ||
151 | if (server.serverNumber === 1) { | ||
152 | expect(rootServer1Get.userId).to.be.a('number') | ||
153 | } else { | ||
154 | expect(rootServer1Get.userId).to.be.undefined | ||
155 | } | ||
156 | |||
151 | await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') | 157 | await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') |
152 | } | 158 | } |
153 | }) | 159 | }) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 8b9c6b455..513bca8a0 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -180,7 +180,7 @@ describe('Test users', function () { | |||
180 | it('Should be able to upload a video again') | 180 | it('Should be able to upload a video again') |
181 | 181 | ||
182 | it('Should be able to create a new user', async function () { | 182 | it('Should be able to create a new user', async function () { |
183 | await createUser(server.url, accessToken, user.username,user.password, 2 * 1024 * 1024) | 183 | await createUser(server.url, accessToken, user.username, user.password, 2 * 1024 * 1024) |
184 | }) | 184 | }) |
185 | 185 | ||
186 | it('Should be able to login with this user', async function () { | 186 | it('Should be able to login with this user', async function () { |
@@ -322,6 +322,40 @@ describe('Test users', function () { | |||
322 | expect(users[ 1 ].nsfwPolicy).to.equal('display') | 322 | expect(users[ 1 ].nsfwPolicy).to.equal('display') |
323 | }) | 323 | }) |
324 | 324 | ||
325 | it('Should search user by username', async function () { | ||
326 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot') | ||
327 | const users = res.body.data as User[] | ||
328 | |||
329 | expect(res.body.total).to.equal(1) | ||
330 | expect(users.length).to.equal(1) | ||
331 | |||
332 | expect(users[ 0 ].username).to.equal('root') | ||
333 | }) | ||
334 | |||
335 | it('Should search user by email', async function () { | ||
336 | { | ||
337 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam') | ||
338 | const users = res.body.data as User[] | ||
339 | |||
340 | expect(res.body.total).to.equal(1) | ||
341 | expect(users.length).to.equal(1) | ||
342 | |||
343 | expect(users[ 0 ].username).to.equal('user_1') | ||
344 | expect(users[ 0 ].email).to.equal('user_1@example.com') | ||
345 | } | ||
346 | |||
347 | { | ||
348 | const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example') | ||
349 | const users = res.body.data as User[] | ||
350 | |||
351 | expect(res.body.total).to.equal(2) | ||
352 | expect(users.length).to.equal(2) | ||
353 | |||
354 | expect(users[ 0 ].username).to.equal('root') | ||
355 | expect(users[ 1 ].username).to.equal('user_1') | ||
356 | } | ||
357 | }) | ||
358 | |||
325 | it('Should update my password', async function () { | 359 | it('Should update my password', async function () { |
326 | await updateMyUser({ | 360 | await updateMyUser({ |
327 | url: server.url, | 361 | url: server.url, |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index bf58f9c79..9bdb78491 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -14,4 +14,6 @@ import './video-nsfw' | |||
14 | import './video-privacy' | 14 | import './video-privacy' |
15 | import './video-schedule-update' | 15 | import './video-schedule-update' |
16 | import './video-transcoder' | 16 | import './video-transcoder' |
17 | import './videos-filter' | ||
18 | import './videos-history' | ||
17 | import './videos-overview' | 19 | import './videos-overview' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 4553ee855..b9ace2885 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -987,19 +987,19 @@ describe('Test multiple servers', function () { | |||
987 | files: [ | 987 | files: [ |
988 | { | 988 | { |
989 | resolution: 720, | 989 | resolution: 720, |
990 | size: 36000 | 990 | size: 72000 |
991 | }, | 991 | }, |
992 | { | 992 | { |
993 | resolution: 480, | 993 | resolution: 480, |
994 | size: 21000 | 994 | size: 45000 |
995 | }, | 995 | }, |
996 | { | 996 | { |
997 | resolution: 360, | 997 | resolution: 360, |
998 | size: 17000 | 998 | size: 34600 |
999 | }, | 999 | }, |
1000 | { | 1000 | { |
1001 | resolution: 240, | 1001 | resolution: 240, |
1002 | size: 13000 | 1002 | size: 24770 |
1003 | } | 1003 | } |
1004 | ] | 1004 | ] |
1005 | } | 1005 | } |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index e3d62b7a0..089c3df25 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -118,7 +118,7 @@ describe('Test a single server', function () { | |||
118 | const categories = res.body | 118 | const categories = res.body |
119 | expect(Object.keys(categories)).to.have.length.above(10) | 119 | expect(Object.keys(categories)).to.have.length.above(10) |
120 | 120 | ||
121 | expect(categories[11]).to.equal('News') | 121 | expect(categories[11]).to.equal('News & Politics') |
122 | }) | 122 | }) |
123 | 123 | ||
124 | it('Should list video licences', async function () { | 124 | it('Should list video licences', async function () { |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index b7866d529..aaee79a4a 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -30,7 +30,7 @@ describe('Test video imports', function () { | |||
30 | const videoHttp: VideoDetails = resHttp.body | 30 | const videoHttp: VideoDetails = resHttp.body |
31 | 31 | ||
32 | expect(videoHttp.name).to.equal('small video - youtube') | 32 | expect(videoHttp.name).to.equal('small video - youtube') |
33 | expect(videoHttp.category.label).to.equal('News') | 33 | expect(videoHttp.category.label).to.equal('News & Politics') |
34 | expect(videoHttp.licence.label).to.equal('Attribution') | 34 | expect(videoHttp.licence.label).to.equal('Attribution') |
35 | expect(videoHttp.language.label).to.equal('Unknown') | 35 | expect(videoHttp.language.label).to.equal('Unknown') |
36 | expect(videoHttp.nsfw).to.be.false | 36 | expect(videoHttp.nsfw).to.be.false |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 0f83d4d57..85795d2ed 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -4,8 +4,8 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import * as ffmpeg from 'fluent-ffmpeg' | 6 | import * as ffmpeg from 'fluent-ffmpeg' |
7 | import { VideoDetails, VideoState } from '../../../../shared/models/videos' | 7 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' |
8 | import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' | 8 | import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
9 | import { | 9 | import { |
10 | buildAbsoluteFixturePath, | 10 | buildAbsoluteFixturePath, |
11 | doubleFollow, | 11 | doubleFollow, |
@@ -18,10 +18,13 @@ import { | |||
18 | ServerInfo, | 18 | ServerInfo, |
19 | setAccessTokensToServers, | 19 | setAccessTokensToServers, |
20 | uploadVideo, | 20 | uploadVideo, |
21 | webtorrentAdd | 21 | webtorrentAdd, |
22 | generateHighBitrateVideo | ||
22 | } from '../../utils' | 23 | } from '../../utils' |
23 | import { join } from 'path' | 24 | import { join } from 'path' |
24 | import { waitJobs } from '../../utils/server/jobs' | 25 | import { waitJobs } from '../../utils/server/jobs' |
26 | import { pathExists } from 'fs-extra' | ||
27 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | ||
25 | 28 | ||
26 | const expect = chai.expect | 29 | const expect = chai.expect |
27 | 30 | ||
@@ -121,7 +124,7 @@ describe('Test video transcoding', function () { | |||
121 | expect(videoDetails.files).to.have.lengthOf(4) | 124 | expect(videoDetails.files).to.have.lengthOf(4) |
122 | 125 | ||
123 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 126 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
124 | const probe = await audio.get(ffmpeg, path) | 127 | const probe = await audio.get(path) |
125 | 128 | ||
126 | if (probe.audioStream) { | 129 | if (probe.audioStream) { |
127 | expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') | 130 | expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') |
@@ -152,7 +155,7 @@ describe('Test video transcoding', function () { | |||
152 | 155 | ||
153 | expect(videoDetails.files).to.have.lengthOf(4) | 156 | expect(videoDetails.files).to.have.lengthOf(4) |
154 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 157 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
155 | const probe = await audio.get(ffmpeg, path) | 158 | const probe = await audio.get(path) |
156 | expect(probe).to.not.have.property('audioStream') | 159 | expect(probe).to.not.have.property('audioStream') |
157 | } | 160 | } |
158 | }) | 161 | }) |
@@ -177,9 +180,9 @@ describe('Test video transcoding', function () { | |||
177 | 180 | ||
178 | expect(videoDetails.files).to.have.lengthOf(4) | 181 | expect(videoDetails.files).to.have.lengthOf(4) |
179 | const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) | 182 | const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) |
180 | const fixtureVideoProbe = await audio.get(ffmpeg, fixturePath) | 183 | const fixtureVideoProbe = await audio.get(fixturePath) |
181 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') | 184 | const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') |
182 | const videoProbe = await audio.get(ffmpeg, path) | 185 | const videoProbe = await audio.get(path) |
183 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { | 186 | if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { |
184 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] | 187 | const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] |
185 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) | 188 | expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) |
@@ -228,7 +231,7 @@ describe('Test video transcoding', function () { | |||
228 | } | 231 | } |
229 | }) | 232 | }) |
230 | 233 | ||
231 | it('Should wait transcoding before publishing the video', async function () { | 234 | it('Should wait for transcoding before publishing the video', async function () { |
232 | this.timeout(80000) | 235 | this.timeout(80000) |
233 | 236 | ||
234 | { | 237 | { |
@@ -281,6 +284,45 @@ describe('Test video transcoding', function () { | |||
281 | } | 284 | } |
282 | }) | 285 | }) |
283 | 286 | ||
287 | it('Should respect maximum bitrate values', async function () { | ||
288 | this.timeout(160000) | ||
289 | |||
290 | let tempFixturePath: string | ||
291 | |||
292 | { | ||
293 | tempFixturePath = await generateHighBitrateVideo() | ||
294 | |||
295 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
296 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) | ||
297 | } | ||
298 | |||
299 | const videoAttributes = { | ||
300 | name: 'high bitrate video', | ||
301 | description: 'high bitrate video', | ||
302 | fixture: tempFixturePath | ||
303 | } | ||
304 | |||
305 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
306 | |||
307 | await waitJobs(servers) | ||
308 | |||
309 | for (const server of servers) { | ||
310 | const res = await getVideosList(server.url) | ||
311 | |||
312 | const video = res.body.data.find(v => v.name === videoAttributes.name) | ||
313 | |||
314 | for (const resolution of ['240', '360', '480', '720', '1080']) { | ||
315 | const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') | ||
316 | const bitrate = await getVideoFileBitrate(path) | ||
317 | const fps = await getVideoFileFPS(path) | ||
318 | const resolution2 = await getVideoFileResolution(path) | ||
319 | |||
320 | expect(resolution2.videoFileResolution.toString()).to.equal(resolution) | ||
321 | expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | ||
322 | } | ||
323 | } | ||
324 | }) | ||
325 | |||
284 | after(async function () { | 326 | after(async function () { |
285 | killallServers(servers) | 327 | killallServers(servers) |
286 | }) | 328 | }) |
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts new file mode 100644 index 000000000..a7588129f --- /dev/null +++ b/server/tests/api/videos/videos-filter.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | killallServers, | ||
11 | makeGetRequest, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo, | ||
15 | userLogin | ||
16 | } from '../../utils' | ||
17 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | ||
18 | import { UserRole } from '../../../../shared/models/users' | ||
19 | |||
20 | const expect = chai.expect | ||
21 | |||
22 | async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) { | ||
23 | const paths = [ | ||
24 | '/api/v1/video-channels/root_channel/videos', | ||
25 | '/api/v1/accounts/root/videos', | ||
26 | '/api/v1/videos', | ||
27 | '/api/v1/search/videos' | ||
28 | ] | ||
29 | |||
30 | const videosResults: Video[][] = [] | ||
31 | |||
32 | for (const path of paths) { | ||
33 | const res = await makeGetRequest({ | ||
34 | url: server.url, | ||
35 | path, | ||
36 | token, | ||
37 | query: { | ||
38 | sort: 'createdAt', | ||
39 | filter | ||
40 | }, | ||
41 | statusCodeExpected | ||
42 | }) | ||
43 | |||
44 | videosResults.push(res.body.data.map(v => v.name)) | ||
45 | } | ||
46 | |||
47 | return videosResults | ||
48 | } | ||
49 | |||
50 | describe('Test videos filter validator', function () { | ||
51 | let servers: ServerInfo[] | ||
52 | |||
53 | // --------------------------------------------------------------- | ||
54 | |||
55 | before(async function () { | ||
56 | this.timeout(120000) | ||
57 | |||
58 | await flushTests() | ||
59 | |||
60 | servers = await flushAndRunMultipleServers(2) | ||
61 | |||
62 | await setAccessTokensToServers(servers) | ||
63 | |||
64 | for (const server of servers) { | ||
65 | const moderator = { username: 'moderator', password: 'my super password' } | ||
66 | await createUser( | ||
67 | server.url, | ||
68 | server.accessToken, | ||
69 | moderator.username, | ||
70 | moderator.password, | ||
71 | undefined, | ||
72 | undefined, | ||
73 | UserRole.MODERATOR | ||
74 | ) | ||
75 | server['moderatorAccessToken'] = await userLogin(server, moderator) | ||
76 | |||
77 | await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber }) | ||
78 | |||
79 | { | ||
80 | const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED } | ||
81 | await uploadVideo(server.url, server.accessToken, attributes) | ||
82 | } | ||
83 | |||
84 | { | ||
85 | const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE } | ||
86 | await uploadVideo(server.url, server.accessToken, attributes) | ||
87 | } | ||
88 | } | ||
89 | |||
90 | await doubleFollow(servers[0], servers[1]) | ||
91 | }) | ||
92 | |||
93 | describe('Check videos filter', function () { | ||
94 | |||
95 | it('Should display local videos', async function () { | ||
96 | for (const server of servers) { | ||
97 | const namesResults = await getVideosNames(server, server.accessToken, 'local') | ||
98 | for (const names of namesResults) { | ||
99 | expect(names).to.have.lengthOf(1) | ||
100 | expect(names[ 0 ]).to.equal('public ' + server.serverNumber) | ||
101 | } | ||
102 | } | ||
103 | }) | ||
104 | |||
105 | it('Should display all local videos by the admin or the moderator', async function () { | ||
106 | for (const server of servers) { | ||
107 | for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) { | ||
108 | |||
109 | const namesResults = await getVideosNames(server, token, 'all-local') | ||
110 | for (const names of namesResults) { | ||
111 | expect(names).to.have.lengthOf(3) | ||
112 | |||
113 | expect(names[ 0 ]).to.equal('public ' + server.serverNumber) | ||
114 | expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber) | ||
115 | expect(names[ 2 ]).to.equal('private ' + server.serverNumber) | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | }) | ||
120 | }) | ||
121 | |||
122 | after(async function () { | ||
123 | killallServers(servers) | ||
124 | |||
125 | // Keep the logs if the test failed | ||
126 | if (this['ok']) { | ||
127 | await flushTests() | ||
128 | } | ||
129 | }) | ||
130 | }) | ||
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts new file mode 100644 index 000000000..6d289b288 --- /dev/null +++ b/server/tests/api/videos/videos-history.ts | |||
@@ -0,0 +1,128 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | flushTests, | ||
7 | getVideosListWithToken, | ||
8 | getVideoWithToken, | ||
9 | killallServers, makePutBodyRequest, | ||
10 | runServer, searchVideoWithToken, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../utils' | ||
15 | import { Video, VideoDetails } from '../../../../shared/models/videos' | ||
16 | import { userWatchVideo } from '../../utils/videos/video-history' | ||
17 | |||
18 | const expect = chai.expect | ||
19 | |||
20 | describe('Test videos history', function () { | ||
21 | let server: ServerInfo = null | ||
22 | let video1UUID: string | ||
23 | let video2UUID: string | ||
24 | let video3UUID: string | ||
25 | |||
26 | before(async function () { | ||
27 | this.timeout(30000) | ||
28 | |||
29 | await flushTests() | ||
30 | |||
31 | server = await runServer(1) | ||
32 | |||
33 | await setAccessTokensToServers([ server ]) | ||
34 | |||
35 | { | ||
36 | const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' }) | ||
37 | video1UUID = res.body.video.uuid | ||
38 | } | ||
39 | |||
40 | { | ||
41 | const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' }) | ||
42 | video2UUID = res.body.video.uuid | ||
43 | } | ||
44 | |||
45 | { | ||
46 | const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) | ||
47 | video3UUID = res.body.video.uuid | ||
48 | } | ||
49 | }) | ||
50 | |||
51 | it('Should get videos, without watching history', async function () { | ||
52 | const res = await getVideosListWithToken(server.url, server.accessToken) | ||
53 | const videos: Video[] = res.body.data | ||
54 | |||
55 | for (const video of videos) { | ||
56 | const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id) | ||
57 | const videoDetails: VideoDetails = resDetail.body | ||
58 | |||
59 | expect(video.userHistory).to.be.undefined | ||
60 | expect(videoDetails.userHistory).to.be.undefined | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | it('Should watch the first and second video', async function () { | ||
65 | await userWatchVideo(server.url, server.accessToken, video1UUID, 3) | ||
66 | await userWatchVideo(server.url, server.accessToken, video2UUID, 8) | ||
67 | }) | ||
68 | |||
69 | it('Should return the correct history when listing, searching and getting videos', async function () { | ||
70 | const videosOfVideos: Video[][] = [] | ||
71 | |||
72 | { | ||
73 | const res = await getVideosListWithToken(server.url, server.accessToken) | ||
74 | videosOfVideos.push(res.body.data) | ||
75 | } | ||
76 | |||
77 | { | ||
78 | const res = await searchVideoWithToken(server.url, 'video', server.accessToken) | ||
79 | videosOfVideos.push(res.body.data) | ||
80 | } | ||
81 | |||
82 | for (const videos of videosOfVideos) { | ||
83 | const video1 = videos.find(v => v.uuid === video1UUID) | ||
84 | const video2 = videos.find(v => v.uuid === video2UUID) | ||
85 | const video3 = videos.find(v => v.uuid === video3UUID) | ||
86 | |||
87 | expect(video1.userHistory).to.not.be.undefined | ||
88 | expect(video1.userHistory.currentTime).to.equal(3) | ||
89 | |||
90 | expect(video2.userHistory).to.not.be.undefined | ||
91 | expect(video2.userHistory.currentTime).to.equal(8) | ||
92 | |||
93 | expect(video3.userHistory).to.be.undefined | ||
94 | } | ||
95 | |||
96 | { | ||
97 | const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID) | ||
98 | const videoDetails: VideoDetails = resDetail.body | ||
99 | |||
100 | expect(videoDetails.userHistory).to.not.be.undefined | ||
101 | expect(videoDetails.userHistory.currentTime).to.equal(3) | ||
102 | } | ||
103 | |||
104 | { | ||
105 | const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID) | ||
106 | const videoDetails: VideoDetails = resDetail.body | ||
107 | |||
108 | expect(videoDetails.userHistory).to.not.be.undefined | ||
109 | expect(videoDetails.userHistory.currentTime).to.equal(8) | ||
110 | } | ||
111 | |||
112 | { | ||
113 | const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID) | ||
114 | const videoDetails: VideoDetails = resDetail.body | ||
115 | |||
116 | expect(videoDetails.userHistory).to.be.undefined | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | after(async function () { | ||
121 | killallServers([ server ]) | ||
122 | |||
123 | // Keep the logs if the test failed | ||
124 | if (this['ok']) { | ||
125 | await flushTests() | ||
126 | } | ||
127 | }) | ||
128 | }) | ||
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts new file mode 100644 index 000000000..66dd39cce --- /dev/null +++ b/server/tests/cli/optimize-old-videos.ts | |||
@@ -0,0 +1,120 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' | ||
6 | import { | ||
7 | doubleFollow, | ||
8 | execCLI, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, generateHighBitrateVideo, | ||
11 | getEnvCli, | ||
12 | getVideo, | ||
13 | getVideosList, | ||
14 | killallServers, root, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | uploadVideo, viewVideo, wait | ||
18 | } from '../utils' | ||
19 | import { waitJobs } from '../utils/server/jobs' | ||
20 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' | ||
21 | import { VIDEO_TRANSCODING_FPS } from '../../initializers' | ||
22 | import { join } from 'path' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | |||
26 | describe('Test optimize old videos', function () { | ||
27 | let servers: ServerInfo[] = [] | ||
28 | let video1UUID: string | ||
29 | let video2UUID: string | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(200000) | ||
33 | |||
34 | await flushTests() | ||
35 | |||
36 | // Run server 2 to have transcoding enabled | ||
37 | servers = await flushAndRunMultipleServers(2) | ||
38 | await setAccessTokensToServers(servers) | ||
39 | |||
40 | await doubleFollow(servers[0], servers[1]) | ||
41 | |||
42 | let tempFixturePath: string | ||
43 | |||
44 | { | ||
45 | tempFixturePath = await generateHighBitrateVideo() | ||
46 | |||
47 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
48 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS)) | ||
49 | } | ||
50 | |||
51 | // Upload two videos for our needs | ||
52 | const res1 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video1', fixture: tempFixturePath }) | ||
53 | video1UUID = res1.body.video.uuid | ||
54 | const res2 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video2', fixture: tempFixturePath }) | ||
55 | video2UUID = res2.body.video.uuid | ||
56 | |||
57 | await waitJobs(servers) | ||
58 | }) | ||
59 | |||
60 | it('Should have two video files on each server', async function () { | ||
61 | this.timeout(30000) | ||
62 | |||
63 | for (const server of servers) { | ||
64 | const res = await getVideosList(server.url) | ||
65 | const videos = res.body.data | ||
66 | expect(videos).to.have.lengthOf(2) | ||
67 | |||
68 | for (const video of videos) { | ||
69 | const res2 = await getVideo(server.url, video.uuid) | ||
70 | const videoDetail: VideoDetails = res2.body | ||
71 | expect(videoDetail.files).to.have.lengthOf(1) | ||
72 | } | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should run optimize script', async function () { | ||
77 | this.timeout(120000) | ||
78 | |||
79 | const env = getEnvCli(servers[0]) | ||
80 | await execCLI(`${env} npm run optimize-old-videos`) | ||
81 | |||
82 | await waitJobs(servers) | ||
83 | |||
84 | for (const server of servers) { | ||
85 | const res = await getVideosList(server.url) | ||
86 | const videos: Video[] = res.body.data | ||
87 | |||
88 | expect(videos).to.have.lengthOf(2) | ||
89 | |||
90 | for (const video of videos) { | ||
91 | await viewVideo(server.url, video.uuid) | ||
92 | |||
93 | // Refresh video | ||
94 | await waitJobs(servers) | ||
95 | await wait(5000) | ||
96 | await waitJobs(servers) | ||
97 | |||
98 | const res2 = await getVideo(server.url, video.uuid) | ||
99 | const videosDetails: VideoDetails = res2.body | ||
100 | |||
101 | expect(videosDetails.files).to.have.lengthOf(1) | ||
102 | const file = videosDetails.files[0] | ||
103 | |||
104 | expect(file.size).to.be.below(5000000) | ||
105 | |||
106 | const path = join(root(), 'test1', 'videos', video.uuid + '-' + file.resolution.id + '.mp4') | ||
107 | const bitrate = await getVideoFileBitrate(path) | ||
108 | const fps = await getVideoFileFPS(path) | ||
109 | const resolution = await getVideoFileResolution(path) | ||
110 | |||
111 | expect(resolution.videoFileResolution).to.equal(file.resolution.id) | ||
112 | expect(bitrate).to.be.below(getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | ||
113 | } | ||
114 | } | ||
115 | }) | ||
116 | |||
117 | after(async function () { | ||
118 | killallServers(servers) | ||
119 | }) | ||
120 | }) | ||
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts new file mode 100644 index 000000000..a6d829a9f --- /dev/null +++ b/server/tests/helpers/core-utils.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | parseBytes | ||
7 | } from '../../helpers/core-utils' | ||
8 | |||
9 | const expect = chai.expect | ||
10 | |||
11 | describe('Parse Bytes', function () { | ||
12 | it('Should pass when given valid value', async function () { | ||
13 | // just return it | ||
14 | expect(parseBytes(1024)).to.be.eq(1024) | ||
15 | expect(parseBytes(1048576)).to.be.eq(1048576) | ||
16 | expect(parseBytes('1024')).to.be.eq(1024) | ||
17 | expect(parseBytes('1048576')).to.be.eq(1048576) | ||
18 | |||
19 | // sizes | ||
20 | expect(parseBytes('1B')).to.be.eq(1024) | ||
21 | expect(parseBytes('1MB')).to.be.eq(1048576) | ||
22 | expect(parseBytes('1GB')).to.be.eq(1073741824) | ||
23 | expect(parseBytes('1TB')).to.be.eq(1099511627776) | ||
24 | |||
25 | expect(parseBytes('5GB')).to.be.eq(5368709120) | ||
26 | expect(parseBytes('5TB')).to.be.eq(5497558138880) | ||
27 | |||
28 | expect(parseBytes('1024B')).to.be.eq(1048576) | ||
29 | expect(parseBytes('1024MB')).to.be.eq(1073741824) | ||
30 | expect(parseBytes('1024GB')).to.be.eq(1099511627776) | ||
31 | expect(parseBytes('1024TB')).to.be.eq(1125899906842624) | ||
32 | |||
33 | // with whitespace | ||
34 | expect(parseBytes('1 GB')).to.be.eq(1073741824) | ||
35 | expect(parseBytes('1\tGB')).to.be.eq(1073741824) | ||
36 | |||
37 | // sum value | ||
38 | expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600) | ||
39 | expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120) | ||
40 | expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880) | ||
41 | expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880) | ||
42 | expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224) | ||
43 | }) | ||
44 | |||
45 | it('Should be invalid when given invalid value', async function () { | ||
46 | expect(parseBytes('6GB 1GB')).to.be.eq(6) | ||
47 | }) | ||
48 | }) | ||
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts new file mode 100644 index 000000000..40c7dc70e --- /dev/null +++ b/server/tests/helpers/index.ts | |||
@@ -0,0 +1 @@ | |||
import './core-utils' | |||
diff --git a/server/tests/utils/miscs/miscs.ts b/server/tests/utils/miscs/miscs.ts index b2f80e9b1..589daa420 100644 --- a/server/tests/utils/miscs/miscs.ts +++ b/server/tests/utils/miscs/miscs.ts | |||
@@ -4,7 +4,8 @@ import * as chai from 'chai' | |||
4 | import { isAbsolute, join } from 'path' | 4 | import { isAbsolute, join } from 'path' |
5 | import * as request from 'supertest' | 5 | import * as request from 'supertest' |
6 | import * as WebTorrent from 'webtorrent' | 6 | import * as WebTorrent from 'webtorrent' |
7 | import { readFile } from 'fs-extra' | 7 | import { pathExists, readFile } from 'fs-extra' |
8 | import * as ffmpeg from 'fluent-ffmpeg' | ||
8 | 9 | ||
9 | const expect = chai.expect | 10 | const expect = chai.expect |
10 | let webtorrent = new WebTorrent() | 11 | let webtorrent = new WebTorrent() |
@@ -51,14 +52,41 @@ async function testImage (url: string, imageName: string, imagePath: string, ext | |||
51 | expect(data.length).to.be.below(maxLength) | 52 | expect(data.length).to.be.below(maxLength) |
52 | } | 53 | } |
53 | 54 | ||
54 | function buildAbsoluteFixturePath (path: string) { | 55 | function buildAbsoluteFixturePath (path: string, customTravisPath = false) { |
55 | if (isAbsolute(path)) { | 56 | if (isAbsolute(path)) { |
56 | return path | 57 | return path |
57 | } | 58 | } |
58 | 59 | ||
60 | if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path) | ||
61 | |||
59 | return join(__dirname, '..', '..', 'fixtures', path) | 62 | return join(__dirname, '..', '..', 'fixtures', path) |
60 | } | 63 | } |
61 | 64 | ||
65 | async function generateHighBitrateVideo () { | ||
66 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) | ||
67 | |||
68 | const exists = await pathExists(tempFixturePath) | ||
69 | if (!exists) { | ||
70 | |||
71 | // Generate a random, high bitrate video on the fly, so we don't have to include | ||
72 | // a large file in the repo. The video needs to have a certain minimum length so | ||
73 | // that FFmpeg properly applies bitrate limits. | ||
74 | // https://stackoverflow.com/a/15795112 | ||
75 | return new Promise<string>(async (res, rej) => { | ||
76 | ffmpeg() | ||
77 | .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) | ||
78 | .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) | ||
79 | .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) | ||
80 | .output(tempFixturePath) | ||
81 | .on('error', rej) | ||
82 | .on('end', () => res(tempFixturePath)) | ||
83 | .run() | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | return tempFixturePath | ||
88 | } | ||
89 | |||
62 | // --------------------------------------------------------------------------- | 90 | // --------------------------------------------------------------------------- |
63 | 91 | ||
64 | export { | 92 | export { |
@@ -68,5 +96,6 @@ export { | |||
68 | immutableAssign, | 96 | immutableAssign, |
69 | testImage, | 97 | testImage, |
70 | buildAbsoluteFixturePath, | 98 | buildAbsoluteFixturePath, |
71 | root | 99 | root, |
100 | generateHighBitrateVideo | ||
72 | } | 101 | } |
diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts index 27a529eda..5796540f7 100644 --- a/server/tests/utils/requests/requests.ts +++ b/server/tests/utils/requests/requests.ts | |||
@@ -37,9 +37,7 @@ function makeDeleteRequest (options: { | |||
37 | 37 | ||
38 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | 38 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) |
39 | 39 | ||
40 | return req | 40 | return req.expect(options.statusCodeExpected) |
41 | .expect('Content-Type', /json/) | ||
42 | .expect(options.statusCodeExpected) | ||
43 | } | 41 | } |
44 | 42 | ||
45 | function makeUploadRequest (options: { | 43 | function makeUploadRequest (options: { |
diff --git a/server/tests/utils/server/follows.ts b/server/tests/utils/server/follows.ts index 8a65a958b..7741757a6 100644 --- a/server/tests/utils/server/follows.ts +++ b/server/tests/utils/server/follows.ts | |||
@@ -2,7 +2,7 @@ import * as request from 'supertest' | |||
2 | import { ServerInfo } from './servers' | 2 | import { ServerInfo } from './servers' |
3 | import { waitJobs } from './jobs' | 3 | import { waitJobs } from './jobs' |
4 | 4 | ||
5 | function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string) { | 5 | function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) { |
6 | const path = '/api/v1/server/followers' | 6 | const path = '/api/v1/server/followers' |
7 | 7 | ||
8 | return request(url) | 8 | return request(url) |
@@ -10,12 +10,13 @@ function getFollowersListPaginationAndSort (url: string, start: number, count: n | |||
10 | .query({ start }) | 10 | .query({ start }) |
11 | .query({ count }) | 11 | .query({ count }) |
12 | .query({ sort }) | 12 | .query({ sort }) |
13 | .query({ search }) | ||
13 | .set('Accept', 'application/json') | 14 | .set('Accept', 'application/json') |
14 | .expect(200) | 15 | .expect(200) |
15 | .expect('Content-Type', /json/) | 16 | .expect('Content-Type', /json/) |
16 | } | 17 | } |
17 | 18 | ||
18 | function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string) { | 19 | function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) { |
19 | const path = '/api/v1/server/following' | 20 | const path = '/api/v1/server/following' |
20 | 21 | ||
21 | return request(url) | 22 | return request(url) |
@@ -23,6 +24,7 @@ function getFollowingListPaginationAndSort (url: string, start: number, count: n | |||
23 | .query({ start }) | 24 | .query({ start }) |
24 | .query({ count }) | 25 | .query({ count }) |
25 | .query({ sort }) | 26 | .query({ sort }) |
27 | .query({ search }) | ||
26 | .set('Accept', 'application/json') | 28 | .set('Accept', 'application/json') |
27 | .expect(200) | 29 | .expect(200) |
28 | .expect('Content-Type', /json/) | 30 | .expect('Content-Type', /json/) |
diff --git a/server/tests/utils/users/blocklist.ts b/server/tests/utils/users/blocklist.ts new file mode 100644 index 000000000..35b537571 --- /dev/null +++ b/server/tests/utils/users/blocklist.ts | |||
@@ -0,0 +1,198 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import { makeDeleteRequest, makePostBodyRequest } from '../index' | ||
4 | import { makeGetRequest } from '../requests/requests' | ||
5 | |||
6 | function getAccountBlocklistByAccount ( | ||
7 | url: string, | ||
8 | token: string, | ||
9 | start: number, | ||
10 | count: number, | ||
11 | sort = '-createdAt', | ||
12 | statusCodeExpected = 200 | ||
13 | ) { | ||
14 | const path = '/api/v1/users/me/blocklist/accounts' | ||
15 | |||
16 | return makeGetRequest({ | ||
17 | url, | ||
18 | token, | ||
19 | query: { start, count, sort }, | ||
20 | path, | ||
21 | statusCodeExpected | ||
22 | }) | ||
23 | } | ||
24 | |||
25 | function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { | ||
26 | const path = '/api/v1/users/me/blocklist/accounts' | ||
27 | |||
28 | return makePostBodyRequest({ | ||
29 | url, | ||
30 | path, | ||
31 | token, | ||
32 | fields: { | ||
33 | accountName: accountToBlock | ||
34 | }, | ||
35 | statusCodeExpected | ||
36 | }) | ||
37 | } | ||
38 | |||
39 | function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { | ||
40 | const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock | ||
41 | |||
42 | return makeDeleteRequest({ | ||
43 | url, | ||
44 | path, | ||
45 | token, | ||
46 | statusCodeExpected | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | function getServerBlocklistByAccount ( | ||
51 | url: string, | ||
52 | token: string, | ||
53 | start: number, | ||
54 | count: number, | ||
55 | sort = '-createdAt', | ||
56 | statusCodeExpected = 200 | ||
57 | ) { | ||
58 | const path = '/api/v1/users/me/blocklist/servers' | ||
59 | |||
60 | return makeGetRequest({ | ||
61 | url, | ||
62 | token, | ||
63 | query: { start, count, sort }, | ||
64 | path, | ||
65 | statusCodeExpected | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
70 | const path = '/api/v1/users/me/blocklist/servers' | ||
71 | |||
72 | return makePostBodyRequest({ | ||
73 | url, | ||
74 | path, | ||
75 | token, | ||
76 | fields: { | ||
77 | host: serverToBlock | ||
78 | }, | ||
79 | statusCodeExpected | ||
80 | }) | ||
81 | } | ||
82 | |||
83 | function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
84 | const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock | ||
85 | |||
86 | return makeDeleteRequest({ | ||
87 | url, | ||
88 | path, | ||
89 | token, | ||
90 | statusCodeExpected | ||
91 | }) | ||
92 | } | ||
93 | |||
94 | function getAccountBlocklistByServer ( | ||
95 | url: string, | ||
96 | token: string, | ||
97 | start: number, | ||
98 | count: number, | ||
99 | sort = '-createdAt', | ||
100 | statusCodeExpected = 200 | ||
101 | ) { | ||
102 | const path = '/api/v1/server/blocklist/accounts' | ||
103 | |||
104 | return makeGetRequest({ | ||
105 | url, | ||
106 | token, | ||
107 | query: { start, count, sort }, | ||
108 | path, | ||
109 | statusCodeExpected | ||
110 | }) | ||
111 | } | ||
112 | |||
113 | function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) { | ||
114 | const path = '/api/v1/server/blocklist/accounts' | ||
115 | |||
116 | return makePostBodyRequest({ | ||
117 | url, | ||
118 | path, | ||
119 | token, | ||
120 | fields: { | ||
121 | accountName: accountToBlock | ||
122 | }, | ||
123 | statusCodeExpected | ||
124 | }) | ||
125 | } | ||
126 | |||
127 | function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) { | ||
128 | const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock | ||
129 | |||
130 | return makeDeleteRequest({ | ||
131 | url, | ||
132 | path, | ||
133 | token, | ||
134 | statusCodeExpected | ||
135 | }) | ||
136 | } | ||
137 | |||
138 | function getServerBlocklistByServer ( | ||
139 | url: string, | ||
140 | token: string, | ||
141 | start: number, | ||
142 | count: number, | ||
143 | sort = '-createdAt', | ||
144 | statusCodeExpected = 200 | ||
145 | ) { | ||
146 | const path = '/api/v1/server/blocklist/servers' | ||
147 | |||
148 | return makeGetRequest({ | ||
149 | url, | ||
150 | token, | ||
151 | query: { start, count, sort }, | ||
152 | path, | ||
153 | statusCodeExpected | ||
154 | }) | ||
155 | } | ||
156 | |||
157 | function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
158 | const path = '/api/v1/server/blocklist/servers' | ||
159 | |||
160 | return makePostBodyRequest({ | ||
161 | url, | ||
162 | path, | ||
163 | token, | ||
164 | fields: { | ||
165 | host: serverToBlock | ||
166 | }, | ||
167 | statusCodeExpected | ||
168 | }) | ||
169 | } | ||
170 | |||
171 | function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) { | ||
172 | const path = '/api/v1/server/blocklist/servers/' + serverToBlock | ||
173 | |||
174 | return makeDeleteRequest({ | ||
175 | url, | ||
176 | path, | ||
177 | token, | ||
178 | statusCodeExpected | ||
179 | }) | ||
180 | } | ||
181 | |||
182 | // --------------------------------------------------------------------------- | ||
183 | |||
184 | export { | ||
185 | getAccountBlocklistByAccount, | ||
186 | addAccountToAccountBlocklist, | ||
187 | removeAccountFromAccountBlocklist, | ||
188 | getServerBlocklistByAccount, | ||
189 | addServerToAccountBlocklist, | ||
190 | removeServerFromAccountBlocklist, | ||
191 | |||
192 | getAccountBlocklistByServer, | ||
193 | addAccountToServerBlocklist, | ||
194 | removeAccountFromServerBlocklist, | ||
195 | getServerBlocklistByServer, | ||
196 | addServerToServerBlocklist, | ||
197 | removeServerFromServerBlocklist | ||
198 | } | ||
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 41d8ce265..d77233d62 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -112,7 +112,7 @@ function getUsersList (url: string, accessToken: string) { | |||
112 | .expect('Content-Type', /json/) | 112 | .expect('Content-Type', /json/) |
113 | } | 113 | } |
114 | 114 | ||
115 | function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) { | 115 | function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) { |
116 | const path = '/api/v1/users' | 116 | const path = '/api/v1/users' |
117 | 117 | ||
118 | return request(url) | 118 | return request(url) |
@@ -120,6 +120,7 @@ function getUsersListPaginationAndSort (url: string, accessToken: string, start: | |||
120 | .query({ start }) | 120 | .query({ start }) |
121 | .query({ count }) | 121 | .query({ count }) |
122 | .query({ sort }) | 122 | .query({ sort }) |
123 | .query({ search }) | ||
123 | .set('Accept', 'application/json') | 124 | .set('Accept', 'application/json') |
124 | .set('Authorization', 'Bearer ' + accessToken) | 125 | .set('Authorization', 'Bearer ' + accessToken) |
125 | .expect(200) | 126 | .expect(200) |
diff --git a/server/tests/utils/videos/video-comments.ts b/server/tests/utils/videos/video-comments.ts index 1b9ee452e..7d4cae364 100644 --- a/server/tests/utils/videos/video-comments.ts +++ b/server/tests/utils/videos/video-comments.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as request from 'supertest' | 1 | import * as request from 'supertest' |
2 | import { makeDeleteRequest } from '../' | 2 | import { makeDeleteRequest } from '../' |
3 | 3 | ||
4 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { | 4 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) { |
5 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | 5 | const path = '/api/v1/videos/' + videoId + '/comment-threads' |
6 | 6 | ||
7 | const req = request(url) | 7 | const req = request(url) |
@@ -10,20 +10,24 @@ function getVideoCommentThreads (url: string, videoId: number | string, start: n | |||
10 | .query({ count: count }) | 10 | .query({ count: count }) |
11 | 11 | ||
12 | if (sort) req.query({ sort }) | 12 | if (sort) req.query({ sort }) |
13 | if (token) req.set('Authorization', 'Bearer ' + token) | ||
13 | 14 | ||
14 | return req.set('Accept', 'application/json') | 15 | return req.set('Accept', 'application/json') |
15 | .expect(200) | 16 | .expect(200) |
16 | .expect('Content-Type', /json/) | 17 | .expect('Content-Type', /json/) |
17 | } | 18 | } |
18 | 19 | ||
19 | function getVideoThreadComments (url: string, videoId: number | string, threadId: number) { | 20 | function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) { |
20 | const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId | 21 | const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId |
21 | 22 | ||
22 | return request(url) | 23 | const req = request(url) |
23 | .get(path) | 24 | .get(path) |
24 | .set('Accept', 'application/json') | 25 | .set('Accept', 'application/json') |
25 | .expect(200) | 26 | |
26 | .expect('Content-Type', /json/) | 27 | if (token) req.set('Authorization', 'Bearer ' + token) |
28 | |||
29 | return req.expect(200) | ||
30 | .expect('Content-Type', /json/) | ||
27 | } | 31 | } |
28 | 32 | ||
29 | function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { | 33 | function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { |
diff --git a/server/tests/utils/videos/video-history.ts b/server/tests/utils/videos/video-history.ts new file mode 100644 index 000000000..7635478f7 --- /dev/null +++ b/server/tests/utils/videos/video-history.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { makePutBodyRequest } from '../requests/requests' | ||
2 | |||
3 | function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) { | ||
4 | const path = '/api/v1/videos/' + videoId + '/watching' | ||
5 | const fields = { currentTime } | ||
6 | |||
7 | return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 }) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | userWatchVideo | ||
14 | } | ||
diff --git a/server/tools/README.md b/server/tools/README.md new file mode 100644 index 000000000..6b94d74e5 --- /dev/null +++ b/server/tools/README.md | |||
@@ -0,0 +1,82 @@ | |||
1 | peertube(8) -- companion CLI for PeerTube | ||
2 | ========================================= | ||
3 | |||
4 | SYNOPSIS | ||
5 | -------- | ||
6 | |||
7 | ``` | ||
8 | peertube [command] [options] | ||
9 | ``` | ||
10 | |||
11 | DESCRIPTION | ||
12 | ----------- | ||
13 | |||
14 | `peertube` wraps various utilities around PeerTube that are used either on a running local, running remote, or cold local instance. | ||
15 | |||
16 | COMMANDS | ||
17 | -------- | ||
18 | |||
19 | Unless otherwise specified, every command can be queried for its own help or manual by passing its name to the `help` command, or by using the `--help` option. | ||
20 | |||
21 | `auth [action]`: stores credentials for your accounts on remote instances so that you don't need to pass them at every command | ||
22 | |||
23 | `upload|up`: upload a video to a remote instance | ||
24 | |||
25 | $ peertube upload \ | ||
26 | -u "PEERTUBE_URL" \ | ||
27 | -U "PEERTUBE_USER" \ | ||
28 | --password "PEERTUBE_PASSWORD" | ||
29 | |||
30 | `import-videos|import`: import a video from a streaming platform to a remote instance | ||
31 | |||
32 | $ peertube import \ | ||
33 | -u "PEERTUBE_URL" \ | ||
34 | -U "PEERTUBE_USER" \ | ||
35 | --password "PEERTUBE_PASSWORD" \ | ||
36 | -t "TARGET_URL" | ||
37 | |||
38 | The target URL can be directly the video file, or any of the supported sites of youtube-dl. The video is downloaded locally and then uploaded. Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection… | ||
39 | |||
40 | `watch|w`: watch a video in the terminal ✩°。⋆ | ||
41 | |||
42 | -g, --gui <player> player type (default: ascii) | ||
43 | -i, --invert invert colors (ascii player only) | ||
44 | -r, --resolution <res> video resolution (default: 720) | ||
45 | |||
46 | It provides support for different players: | ||
47 | |||
48 | - ascii (default ; plays in ascii art in your terminal!) | ||
49 | - mpv | ||
50 | - mplayer | ||
51 | - vlc | ||
52 | - stdout | ||
53 | - xbmc | ||
54 | - airplay | ||
55 | - chromecast | ||
56 | |||
57 | `repl`: interact with the application libraries and objects even when PeerTube is not running | ||
58 | |||
59 | Type .help to see the repl-only functions, or to see the available PeerTube core functions: | ||
60 | |||
61 | repl> lodash.keys(context) | ||
62 | |||
63 | `help [cmd]`: display help for [cmd] | ||
64 | |||
65 | EXAMPLES | ||
66 | -------- | ||
67 | |||
68 | $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD" | ||
69 | $ peertube up <videoFile> | ||
70 | $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10 | ||
71 | |||
72 | SEE ALSO | ||
73 | -------- | ||
74 | |||
75 | [PeerTube Tools Documentation](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md) | ||
76 | |||
77 | [PeerTube Admin Documentation](https://docs.joinpeertube.org/lang/en/docs/) | ||
78 | |||
79 | REPORTING BUGS | ||
80 | -------------- | ||
81 | |||
82 | See [PeerTube repository](https://github.com/Chocobozzz/PeerTube). | ||
diff --git a/server/tools/cli.ts b/server/tools/cli.ts index 53b20964e..108c44218 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts | |||
@@ -23,7 +23,7 @@ async function getSettings () { | |||
23 | if (err) { | 23 | if (err) { |
24 | return rej(err) | 24 | return rej(err) |
25 | } | 25 | } |
26 | return res(data || settings) | 26 | return res(Object.keys(data).length === 0 ? settings : data) |
27 | }) | 27 | }) |
28 | }) | 28 | }) |
29 | } | 29 | } |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 13090a028..21505b79d 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -10,6 +10,7 @@ import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo | |||
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import * as prompt from 'prompt' | 11 | import * as prompt from 'prompt' |
12 | import { remove } from 'fs-extra' | 12 | import { remove } from 'fs-extra' |
13 | import { sha256 } from '../helpers/core-utils' | ||
13 | import { safeGetYoutubeDL } from '../helpers/youtube-dl' | 14 | import { safeGetYoutubeDL } from '../helpers/youtube-dl' |
14 | import { getSettings, netrc } from './cli' | 15 | import { getSettings, netrc } from './cli' |
15 | 16 | ||
@@ -133,8 +134,7 @@ async function run (user, url: string) { | |||
133 | await processVideo(info, program['language'], processOptions.cwd, url, user) | 134 | await processVideo(info, program['language'], processOptions.cwd, url, user) |
134 | } | 135 | } |
135 | 136 | ||
136 | // https://www.youtube.com/watch?v=2Upx39TBc1s | 137 | console.log('Video/s for user %s imported: %s', program['username'], program['targetUrl']) |
137 | console.log('I\'m finished!') | ||
138 | process.exit(0) | 138 | process.exit(0) |
139 | }) | 139 | }) |
140 | } | 140 | } |
@@ -155,7 +155,7 @@ function processVideo (info: any, languageCode: string, cwd: string, url: string | |||
155 | return res() | 155 | return res() |
156 | } | 156 | } |
157 | 157 | ||
158 | const path = join(cwd, new Date().getTime() + '.mp4') | 158 | const path = join(cwd, sha256(videoInfo.url) + '.mp4') |
159 | 159 | ||
160 | console.log('Downloading video "%s"...', videoInfo.title) | 160 | console.log('Downloading video "%s"...', videoInfo.title) |
161 | 161 | ||
@@ -192,7 +192,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st | |||
192 | 192 | ||
193 | let thumbnailfile | 193 | let thumbnailfile |
194 | if (videoInfo.thumbnail) { | 194 | if (videoInfo.thumbnail) { |
195 | thumbnailfile = join(cwd, 'thumbnail.jpg') | 195 | thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') |
196 | 196 | ||
197 | await doRequestAndSaveToFile({ | 197 | await doRequestAndSaveToFile({ |
198 | method: 'GET', | 198 | method: 'GET', |
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts new file mode 100644 index 000000000..6800ff8ab --- /dev/null +++ b/server/tools/peertube-repl.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | import * as repl from 'repl' | ||
2 | import * as path from 'path' | ||
3 | import * as _ from 'lodash' | ||
4 | import * as uuidv1 from 'uuid/v1' | ||
5 | import * as uuidv3 from 'uuid/v3' | ||
6 | import * as uuidv4 from 'uuid/v4' | ||
7 | import * as uuidv5 from 'uuid/v5' | ||
8 | import * as Sequelize from 'sequelize' | ||
9 | import * as YoutubeDL from 'youtube-dl' | ||
10 | |||
11 | import { initDatabaseModels, sequelizeTypescript } from '../initializers' | ||
12 | import * as cli from '../tools/cli' | ||
13 | import { logger } from '../helpers/logger' | ||
14 | import * as constants from '../initializers/constants' | ||
15 | import * as modelsUtils from '../models/utils' | ||
16 | import * as coreUtils from '../helpers/core-utils' | ||
17 | import * as ffmpegUtils from '../helpers/ffmpeg-utils' | ||
18 | import * as peertubeCryptoUtils from '../helpers/peertube-crypto' | ||
19 | import * as signupUtils from '../helpers/signup' | ||
20 | import * as utils from '../helpers/utils' | ||
21 | import * as YoutubeDLUtils from '../helpers/youtube-dl' | ||
22 | |||
23 | let versionCommitHash | ||
24 | |||
25 | const start = async () => { | ||
26 | await initDatabaseModels(true) | ||
27 | |||
28 | await utils.getVersion().then((data) => { | ||
29 | versionCommitHash = data | ||
30 | }) | ||
31 | |||
32 | const initContext = (replServer) => { | ||
33 | return (context) => { | ||
34 | const properties = { | ||
35 | context, repl: replServer, env: process.env, | ||
36 | lodash: _, path, | ||
37 | uuidv1, uuidv3, uuidv4, uuidv5, | ||
38 | cli, logger, constants, | ||
39 | Sequelize, sequelizeTypescript, modelsUtils, | ||
40 | models: sequelizeTypescript.models, transaction: sequelizeTypescript.transaction, | ||
41 | query: sequelizeTypescript.query, queryInterface: sequelizeTypescript.getQueryInterface(), | ||
42 | YoutubeDL, | ||
43 | coreUtils, ffmpegUtils, peertubeCryptoUtils, signupUtils, utils, YoutubeDLUtils | ||
44 | } | ||
45 | |||
46 | for (let prop in properties) { | ||
47 | Object.defineProperty(context, prop, { | ||
48 | configurable: false, | ||
49 | enumerable: true, | ||
50 | value: properties[prop] | ||
51 | }) | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | |||
56 | const replServer = repl.start({ | ||
57 | prompt: `PeerTube [${cli.version}] (${versionCommitHash})> ` | ||
58 | }) | ||
59 | |||
60 | initContext(replServer)(replServer.context) | ||
61 | replServer.on('reset', initContext(replServer)) | ||
62 | |||
63 | const resetCommand = { | ||
64 | help: 'Reset REPL', | ||
65 | action () { | ||
66 | this.write('.clear\n') | ||
67 | this.displayPrompt() | ||
68 | } | ||
69 | } | ||
70 | replServer.defineCommand('reset', resetCommand) | ||
71 | replServer.defineCommand('r', resetCommand) | ||
72 | |||
73 | } | ||
74 | |||
75 | start().then((data) => { | ||
76 | // do nothing | ||
77 | }).catch((err) => { | ||
78 | console.error(err) | ||
79 | }) | ||
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts index ad76bafb4..c8b9fa744 100755 --- a/server/tools/peertube.ts +++ b/server/tools/peertube.ts | |||
@@ -17,6 +17,7 @@ program | |||
17 | .command('import-videos', 'import a video from a streaming platform').alias('import') | 17 | .command('import-videos', 'import a video from a streaming platform').alias('import') |
18 | .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') | 18 | .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') |
19 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') | 19 | .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') |
20 | .command('repl', 'initiate a REPL to access internals') | ||
20 | 21 | ||
21 | /* Not Yet Implemented */ | 22 | /* Not Yet Implemented */ |
22 | program | 23 | program |