diff options
Diffstat (limited to 'server')
42 files changed, 2728 insertions, 98 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 8e3f60010..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, |
@@ -90,7 +91,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n | |||
90 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 91 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
91 | withFiles: false, | 92 | withFiles: false, |
92 | accountId: account.id, | 93 | accountId: account.id, |
93 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 94 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
94 | }) | 95 | }) |
95 | 96 | ||
96 | 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 a8a6cfb08..534305ba6 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts | |||
@@ -119,7 +119,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response) | |||
119 | includeLocalVideos: true, | 119 | includeLocalVideos: true, |
120 | nsfw: buildNSFWFilter(res, query.nsfw), | 120 | nsfw: buildNSFWFilter(res, query.nsfw), |
121 | filter: query.filter, | 121 | filter: query.filter, |
122 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 122 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
123 | }) | 123 | }) |
124 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) | 124 | const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) |
125 | 125 | ||
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 4f8137c03..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', |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 3c511dc70..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)) |
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 c84d1be58..9bf3c5fd8 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -219,7 +219,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
219 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 219 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
220 | withFiles: false, | 220 | withFiles: false, |
221 | videoChannelId: videoChannelInstance.id, | 221 | videoChannelId: videoChannelInstance.id, |
222 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 222 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
223 | }) | 223 | }) |
224 | 224 | ||
225 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 225 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 4f2b4faee..3875c8f79 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -8,7 +8,7 @@ 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 |
@@ -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 6a73e13d0..664154406 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -437,7 +437,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex | |||
437 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 437 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
438 | filter: req.query.filter as VideoFilter, | 438 | filter: req.query.filter as VideoFilter, |
439 | withFiles: false, | 439 | withFiles: false, |
440 | userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 440 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined |
441 | }) | 441 | }) |
442 | 442 | ||
443 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 443 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index a964abdd4..17f35fe8d 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -116,28 +116,27 @@ type TranscodeOptions = { | |||
116 | 116 | ||
117 | function transcode (options: TranscodeOptions) { | 117 | function transcode (options: TranscodeOptions) { |
118 | return new Promise<void>(async (res, rej) => { | 118 | return new Promise<void>(async (res, rej) => { |
119 | let fps = await getVideoFileFPS(options.inputPath) | ||
120 | // On small/medium resolutions, limit FPS | ||
121 | if (options.resolution !== undefined && | ||
122 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | ||
123 | fps > VIDEO_TRANSCODING_FPS.AVERAGE) { | ||
124 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | ||
125 | } | ||
126 | |||
119 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 127 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
120 | .output(options.outputPath) | 128 | .output(options.outputPath) |
121 | .preset(standard) | 129 | command = await presetH264(command, options.resolution, fps) |
122 | 130 | ||
123 | if (CONFIG.TRANSCODING.THREADS > 0) { | 131 | if (CONFIG.TRANSCODING.THREADS > 0) { |
124 | // if we don't set any threads ffmpeg will chose automatically | 132 | // if we don't set any threads ffmpeg will chose automatically |
125 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | 133 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) |
126 | } | 134 | } |
127 | 135 | ||
128 | let fps = await getVideoFileFPS(options.inputPath) | ||
129 | if (options.resolution !== undefined) { | 136 | if (options.resolution !== undefined) { |
130 | // '?x720' or '720x?' for example | 137 | // '?x720' or '720x?' for example |
131 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | 138 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` |
132 | command = command.size(size) | 139 | command = command.size(size) |
133 | |||
134 | // On small/medium resolutions, limit FPS | ||
135 | if ( | ||
136 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | ||
137 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | ||
138 | ) { | ||
139 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | ||
140 | } | ||
141 | } | 140 | } |
142 | 141 | ||
143 | if (fps) { | 142 | if (fps) { |
@@ -148,12 +147,6 @@ function transcode (options: TranscodeOptions) { | |||
148 | command = command.withFPS(fps) | 147 | command = command.withFPS(fps) |
149 | } | 148 | } |
150 | 149 | ||
151 | // Constrained Encoding (VBV) | ||
152 | // https://slhck.info/video/2017/03/01/rate-control.html | ||
153 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | ||
154 | const targetBitrate = getTargetBitrate(options.resolution, fps, VIDEO_TRANSCODING_FPS) | ||
155 | command.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) | ||
156 | |||
157 | command | 150 | command |
158 | .on('error', (err, stdout, stderr) => { | 151 | .on('error', (err, stdout, stderr) => { |
159 | logger.error('Error in transcoding job.', { stdout, stderr }) | 152 | logger.error('Error in transcoding job.', { stdout, stderr }) |
@@ -199,9 +192,9 @@ function getVideoFileStream (path: string) { | |||
199 | * and quality. Superfast and ultrafast will give you better | 192 | * and quality. Superfast and ultrafast will give you better |
200 | * performance, but then quality is noticeably worse. | 193 | * performance, but then quality is noticeably worse. |
201 | */ | 194 | */ |
202 | function veryfast (_ffmpeg) { | 195 | async function presetH264VeryFast (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { |
203 | _ffmpeg | 196 | const localFfmpeg = await presetH264(ffmpeg, resolution, fps) |
204 | .preset(standard) | 197 | localFfmpeg |
205 | .outputOption('-preset:v veryfast') | 198 | .outputOption('-preset:v veryfast') |
206 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | 199 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) |
207 | /* | 200 | /* |
@@ -220,9 +213,9 @@ function veryfast (_ffmpeg) { | |||
220 | /** | 213 | /** |
221 | * A preset optimised for a stillimage audio video | 214 | * A preset optimised for a stillimage audio video |
222 | */ | 215 | */ |
223 | function audio (_ffmpeg) { | 216 | async function presetStillImageWithAudio (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { |
224 | _ffmpeg | 217 | const localFfmpeg = await presetH264VeryFast(ffmpeg, resolution, fps) |
225 | .preset(veryfast) | 218 | localFfmpeg |
226 | .outputOption('-tune stillimage') | 219 | .outputOption('-tune stillimage') |
227 | } | 220 | } |
228 | 221 | ||
@@ -290,8 +283,8 @@ namespace audio { | |||
290 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 283 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
291 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 284 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
292 | */ | 285 | */ |
293 | async function standard (_ffmpeg) { | 286 | async function presetH264 (ffmpeg: ffmpeg, resolution: VideoResolution, fps: number): ffmpeg { |
294 | let localFfmpeg = _ffmpeg | 287 | let localFfmpeg = ffmpeg |
295 | .format('mp4') | 288 | .format('mp4') |
296 | .videoCodec('libx264') | 289 | .videoCodec('libx264') |
297 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | 290 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution |
@@ -324,5 +317,16 @@ async function standard (_ffmpeg) { | |||
324 | 317 | ||
325 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) | 318 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) |
326 | 319 | ||
320 | // Constrained Encoding (VBV) | ||
321 | // https://slhck.info/video/2017/03/01/rate-control.html | ||
322 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | ||
323 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | ||
324 | localFfmpeg.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) | ||
325 | |||
326 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. | ||
327 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
328 | // https://superuser.com/a/908325 | ||
329 | localFfmpeg.outputOption(`-g ${ fps * 2 }`) | ||
330 | |||
327 | return localFfmpeg | 331 | return localFfmpeg |
328 | } | 332 | } |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 39afb4e7b..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) { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index c3e4fcede..03158e356 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -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 = { |
@@ -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 482c03b31..dd5b9bf67 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -29,6 +29,8 @@ 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' | 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' | ||
32 | 34 | ||
33 | 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 |
34 | 36 | ||
@@ -91,7 +93,9 @@ async function initDatabaseModels (silent: boolean) { | |||
91 | VideoImportModel, | 93 | VideoImportModel, |
92 | VideoViewModel, | 94 | VideoViewModel, |
93 | VideoRedundancyModel, | 95 | VideoRedundancyModel, |
94 | UserVideoHistoryModel | 96 | UserVideoHistoryModel, |
97 | AccountBlocklistModel, | ||
98 | ServerBlocklistModel | ||
95 | ]) | 99 | ]) |
96 | 100 | ||
97 | // Check extensions exist in the database | 101 | // Check extensions exist in the database |
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/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/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 17226614c..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' |
@@ -10,3 +11,4 @@ export * from './user-subscriptions' | |||
10 | export * from './videos' | 11 | export * from './videos' |
11 | export * from './webfinger' | 12 | export * from './webfinger' |
12 | export * from './search' | 13 | export * from './search' |
14 | export * from './server' | ||
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/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/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.ts b/server/models/video/video.ts index 4f3f75613..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' |
@@ -93,6 +93,7 @@ import { | |||
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' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | ||
96 | 97 | ||
97 | // 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 |
98 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 99 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -138,6 +139,7 @@ type ForAPIOptions = { | |||
138 | } | 139 | } |
139 | 140 | ||
140 | type AvailableForListIDsOptions = { | 141 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | ||
141 | actorId: number | 143 | actorId: number |
142 | includeLocalVideos: boolean | 144 | includeLocalVideos: boolean |
143 | filter?: VideoFilter | 145 | filter?: VideoFilter |
@@ -151,6 +153,7 @@ type AvailableForListIDsOptions = { | |||
151 | accountId?: number | 153 | accountId?: number |
152 | videoChannelId?: number | 154 | videoChannelId?: number |
153 | trendingDays?: number | 155 | trendingDays?: number |
156 | user?: UserModel | ||
154 | } | 157 | } |
155 | 158 | ||
156 | @Scopes({ | 159 | @Scopes({ |
@@ -235,6 +238,15 @@ type AvailableForListIDsOptions = { | |||
235 | ) | 238 | ) |
236 | } | 239 | } |
237 | ] | 240 | ] |
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 | ) | ||
238 | } | 250 | } |
239 | }, | 251 | }, |
240 | include: [] | 252 | include: [] |
@@ -975,10 +987,10 @@ export class VideoModel extends Model<VideoModel> { | |||
975 | videoChannelId?: number, | 987 | videoChannelId?: number, |
976 | actorId?: number | 988 | actorId?: number |
977 | trendingDays?: number, | 989 | trendingDays?: number, |
978 | userId?: number | 990 | user?: UserModel |
979 | }, countVideos = true) { | 991 | }, countVideos = true) { |
980 | if (options.filter && options.filter === 'all-local' && !options.userId) { | 992 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
981 | throw new Error('Try to filter all-local but no userId is provided') | 993 | throw new Error('Try to filter all-local but no user has not the see all videos right') |
982 | } | 994 | } |
983 | 995 | ||
984 | const query: IFindOptions<VideoModel> = { | 996 | const query: IFindOptions<VideoModel> = { |
@@ -994,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> { | |||
994 | query.group = 'VideoModel.id' | 1006 | query.group = 'VideoModel.id' |
995 | } | 1007 | } |
996 | 1008 | ||
1009 | const serverActor = await getServerActor() | ||
1010 | |||
997 | // actorId === null has a meaning, so just check undefined | 1011 | // actorId === null has a meaning, so just check undefined |
998 | const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id | 1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id |
999 | 1013 | ||
1000 | const queryOptions = { | 1014 | const queryOptions = { |
1001 | actorId, | 1015 | actorId, |
1016 | serverAccountId: serverActor.Account.id, | ||
1002 | nsfw: options.nsfw, | 1017 | nsfw: options.nsfw, |
1003 | categoryOneOf: options.categoryOneOf, | 1018 | categoryOneOf: options.categoryOneOf, |
1004 | licenceOneOf: options.licenceOneOf, | 1019 | licenceOneOf: options.licenceOneOf, |
@@ -1010,7 +1025,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1010 | accountId: options.accountId, | 1025 | accountId: options.accountId, |
1011 | videoChannelId: options.videoChannelId, | 1026 | videoChannelId: options.videoChannelId, |
1012 | includeLocalVideos: options.includeLocalVideos, | 1027 | includeLocalVideos: options.includeLocalVideos, |
1013 | userId: options.userId, | 1028 | user: options.user, |
1014 | trendingDays | 1029 | trendingDays |
1015 | } | 1030 | } |
1016 | 1031 | ||
@@ -1033,7 +1048,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1033 | tagsAllOf?: string[] | 1048 | tagsAllOf?: string[] |
1034 | durationMin?: number // seconds | 1049 | durationMin?: number // seconds |
1035 | durationMax?: number // seconds | 1050 | durationMax?: number // seconds |
1036 | userId?: number, | 1051 | user?: UserModel, |
1037 | filter?: VideoFilter | 1052 | filter?: VideoFilter |
1038 | }) { | 1053 | }) { |
1039 | const whereAnd = [] | 1054 | const whereAnd = [] |
@@ -1104,6 +1119,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1104 | const serverActor = await getServerActor() | 1119 | const serverActor = await getServerActor() |
1105 | const queryOptions = { | 1120 | const queryOptions = { |
1106 | actorId: serverActor.id, | 1121 | actorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | ||
1107 | includeLocalVideos: options.includeLocalVideos, | 1123 | includeLocalVideos: options.includeLocalVideos, |
1108 | nsfw: options.nsfw, | 1124 | nsfw: options.nsfw, |
1109 | categoryOneOf: options.categoryOneOf, | 1125 | categoryOneOf: options.categoryOneOf, |
@@ -1111,7 +1127,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1111 | languageOneOf: options.languageOneOf, | 1127 | languageOneOf: options.languageOneOf, |
1112 | tagsOneOf: options.tagsOneOf, | 1128 | tagsOneOf: options.tagsOneOf, |
1113 | tagsAllOf: options.tagsAllOf, | 1129 | tagsAllOf: options.tagsAllOf, |
1114 | userId: options.userId, | 1130 | user: options.user, |
1115 | filter: options.filter | 1131 | filter: options.filter |
1116 | } | 1132 | } |
1117 | 1133 | ||
@@ -1239,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1239 | 1255 | ||
1240 | // 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 |
1241 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1257 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1242 | const actorId = (await getServerActor()).id | 1258 | const serverActor = await getServerActor() |
1259 | const actorId = serverActor.id | ||
1243 | 1260 | ||
1244 | const scopeOptions = { | 1261 | const scopeOptions: AvailableForListIDsOptions = { |
1262 | serverAccountId: serverActor.Account.id, | ||
1245 | actorId, | 1263 | actorId, |
1246 | includeLocalVideos: true | 1264 | includeLocalVideos: true |
1247 | } | 1265 | } |
@@ -1287,7 +1305,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1287 | 1305 | ||
1288 | private static async getAvailableForApi ( | 1306 | private static async getAvailableForApi ( |
1289 | query: IFindOptions<VideoModel>, | 1307 | query: IFindOptions<VideoModel>, |
1290 | options: AvailableForListIDsOptions & { userId?: number}, | 1308 | options: AvailableForListIDsOptions, |
1291 | countVideos = true | 1309 | countVideos = true |
1292 | ) { | 1310 | ) { |
1293 | const idsScope = { | 1311 | const idsScope = { |
@@ -1320,8 +1338,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1320 | } | 1338 | } |
1321 | ] | 1339 | ] |
1322 | 1340 | ||
1323 | if (options.userId) { | 1341 | if (options.user) { |
1324 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] }) | 1342 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) |
1325 | } | 1343 | } |
1326 | 1344 | ||
1327 | 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 bfc550ae5..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' |
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/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/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/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/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/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/tools/repl.ts b/server/tools/repl.ts new file mode 100644 index 000000000..6800ff8ab --- /dev/null +++ b/server/tools/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 | }) | ||