aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts3
-rw-r--r--server/controllers/api/accounts.ts7
-rw-r--r--server/controllers/api/search.ts4
-rw-r--r--server/controllers/api/server/follows.ts16
-rw-r--r--server/controllers/api/server/index.ts2
-rw-r--r--server/controllers/api/server/server-blocklist.ts132
-rw-r--r--server/controllers/api/users/index.ts4
-rw-r--r--server/controllers/api/users/me.ts4
-rw-r--r--server/controllers/api/users/my-blocklist.ts125
-rw-r--r--server/controllers/api/video-channel.ts4
-rw-r--r--server/controllers/api/videos/captions.ts6
-rw-r--r--server/controllers/api/videos/comment.ts18
-rw-r--r--server/controllers/api/videos/index.ts6
-rw-r--r--server/controllers/api/videos/watching.ts36
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/helpers/core-utils.ts48
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts7
-rw-r--r--server/helpers/custom-validators/users.ts5
-rw-r--r--server/helpers/custom-validators/videos.ts13
-rw-r--r--server/helpers/express-utils.ts3
-rw-r--r--server/helpers/ffmpeg-utils.ts167
-rw-r--r--server/helpers/peertube-crypto.ts5
-rw-r--r--server/helpers/utils.ts22
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/initializers/checker-before-init.ts8
-rw-r--r--server/initializers/constants.ts27
-rw-r--r--server/initializers/database.ts8
-rw-r--r--server/initializers/migrations/0120-video-null.ts3
-rw-r--r--server/initializers/migrations/0195-support.ts9
-rw-r--r--server/initializers/migrations/0245-user-blocked.ts3
-rw-r--r--server/initializers/migrations/0250-video-abuse-state.ts3
-rw-r--r--server/initializers/migrations/0255-video-blacklist-reason.ts3
-rw-r--r--server/initializers/migrations/0260-upload-quota-daily.ts1
-rw-r--r--server/initializers/migrations/0280-webtorrent-policy-user.ts28
-rw-r--r--server/initializers/migrations/0285-description-support.ts53
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/activitypub/videos.ts9
-rw-r--r--server/lib/blocklist.ts40
-rw-r--r--server/lib/client-html.ts2
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/redis.ts54
-rw-r--r--server/lib/video-comment.ts6
-rw-r--r--server/lib/video-transcoding.ts9
-rw-r--r--server/middlewares/cache.ts2
-rw-r--r--server/middlewares/validators/blocklist.ts172
-rw-r--r--server/middlewares/validators/index.ts6
-rw-r--r--server/middlewares/validators/search.ts38
-rw-r--r--server/middlewares/validators/server.ts33
-rw-r--r--server/middlewares/validators/sort.ts8
-rw-r--r--server/middlewares/validators/videos/index.ts8
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts (renamed from server/middlewares/validators/video-abuses.ts)10
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts (renamed from server/middlewares/validators/video-blacklist.ts)10
-rw-r--r--server/middlewares/validators/videos/video-captions.ts (renamed from server/middlewares/validators/video-captions.ts)16
-rw-r--r--server/middlewares/validators/videos/video-channels.ts (renamed from server/middlewares/validators/video-channels.ts)18
-rw-r--r--server/middlewares/validators/videos/video-comments.ts (renamed from server/middlewares/validators/video-comments.ts)18
-rw-r--r--server/middlewares/validators/videos/video-imports.ts (renamed from server/middlewares/validators/video-imports.ts)16
-rw-r--r--server/middlewares/validators/videos/video-watch.ts28
-rw-r--r--server/middlewares/validators/videos/videos.ts (renamed from server/middlewares/validators/videos.ts)101
-rw-r--r--server/models/account/account-blocklist.ts111
-rw-r--r--server/models/account/account.ts3
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/account/user.ts33
-rw-r--r--server/models/activitypub/actor-follow.ts90
-rw-r--r--server/models/redundancy/video-redundancy.ts1
-rw-r--r--server/models/server/server-blocklist.ts121
-rw-r--r--server/models/server/server.ts6
-rw-r--r--server/models/utils.ts16
-rw-r--r--server/models/video/video-comment.ts95
-rw-r--r--server/models/video/video-format-utils.ts13
-rw-r--r--server/models/video/video.ts137
-rw-r--r--server/tests/api/check-params/blocklist.ts494
-rw-r--r--server/tests/api/check-params/index.ts3
-rw-r--r--server/tests/api/check-params/users.ts2
-rw-r--r--server/tests/api/check-params/video-channels.ts8
-rw-r--r--server/tests/api/check-params/video-imports.ts2
-rw-r--r--server/tests/api/check-params/videos-filter.ts127
-rw-r--r--server/tests/api/check-params/videos-history.ts79
-rw-r--r--server/tests/api/check-params/videos.ts4
-rw-r--r--server/tests/api/index-4.ts1
-rw-r--r--server/tests/api/index.ts1
-rw-r--r--server/tests/api/redundancy/index.ts1
-rw-r--r--server/tests/api/redundancy/redundancy.ts483
-rw-r--r--server/tests/api/server/follows.ts40
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/server/redundancy.ts20
-rw-r--r--server/tests/api/users/blocklist.ts511
-rw-r--r--server/tests/api/users/index.ts1
-rw-r--r--server/tests/api/users/users-multiple-servers.ts6
-rw-r--r--server/tests/api/users/users.ts36
-rw-r--r--server/tests/api/videos/index.ts2
-rw-r--r--server/tests/api/videos/multiple-servers.ts8
-rw-r--r--server/tests/api/videos/single-server.ts2
-rw-r--r--server/tests/api/videos/video-imports.ts2
-rw-r--r--server/tests/api/videos/video-transcoder.ts58
-rw-r--r--server/tests/api/videos/videos-filter.ts130
-rw-r--r--server/tests/api/videos/videos-history.ts128
-rw-r--r--server/tests/cli/optimize-old-videos.ts120
-rw-r--r--server/tests/helpers/core-utils.ts48
-rw-r--r--server/tests/helpers/index.ts1
-rw-r--r--server/tests/utils/miscs/miscs.ts35
-rw-r--r--server/tests/utils/requests/requests.ts4
-rw-r--r--server/tests/utils/server/follows.ts6
-rw-r--r--server/tests/utils/users/blocklist.ts198
-rw-r--r--server/tests/utils/users/users.ts3
-rw-r--r--server/tests/utils/videos/video-comments.ts14
-rw-r--r--server/tests/utils/videos/video-history.ts14
-rw-r--r--server/tools/README.md82
-rw-r--r--server/tools/cli.ts2
-rw-r--r--server/tools/peertube-import-videos.ts8
-rw-r--r--server/tools/peertube-repl.ts79
-rwxr-xr-xserver/tools/peertube.ts1
111 files changed, 4438 insertions, 415 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 6229c44aa..433186179 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -13,8 +13,7 @@ import {
13 localVideoChannelValidator, 13 localVideoChannelValidator,
14 videosCustomGetValidator 14 videosCustomGetValidator
15} from '../../middlewares' 15} from '../../middlewares'
16import { videosGetValidator, videosShareValidator } from '../../middlewares/validators' 16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
17import { videoCommentGetValidator } from '../../middlewares/validators/video-comments'
18import { AccountModel } from '../../models/account/account' 17import { AccountModel } from '../../models/account/account'
19import { ActorModel } from '../../models/activitypub/actor' 18import { ActorModel } from '../../models/activitypub/actor'
20import { ActorFollowModel } from '../../models/activitypub/actor-follow' 19import { ActorFollowModel } from '../../models/activitypub/actor-follow'
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index b7691ccba..86ef2aed1 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,7 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, commonVideosFiltersValidator, 4 asyncMiddleware,
5 commonVideosFiltersValidator,
5 listVideoAccountChannelsValidator, 6 listVideoAccountChannelsValidator,
6 optionalAuthenticate, 7 optionalAuthenticate,
7 paginationValidator, 8 paginationValidator,
@@ -86,9 +87,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
86 languageOneOf: req.query.languageOneOf, 87 languageOneOf: req.query.languageOneOf,
87 tagsOneOf: req.query.tagsOneOf, 88 tagsOneOf: req.query.tagsOneOf,
88 tagsAllOf: req.query.tagsAllOf, 89 tagsAllOf: req.query.tagsAllOf,
90 filter: req.query.filter,
89 nsfw: buildNSFWFilter(res, req.query.nsfw), 91 nsfw: buildNSFWFilter(res, req.query.nsfw),
90 withFiles: false, 92 withFiles: false,
91 accountId: account.id 93 accountId: account.id,
94 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
92 }) 95 })
93 96
94 return res.json(getFormattedObjects(resultList.data, resultList.total)) 97 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index fd4db7a54..534305ba6 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -117,7 +117,9 @@ function searchVideos (req: express.Request, res: express.Response) {
117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
118 const options = Object.assign(query, { 118 const options = Object.assign(query, {
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw) 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 filter: query.filter,
122 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
121 }) 123 })
122 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
123 125
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index d62400e42..9fa6c34ba 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -61,14 +61,26 @@ export {
61 61
62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { 62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
63 const serverActor = await getServerActor() 63 const serverActor = await getServerActor()
64 const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 64 const resultList = await ActorFollowModel.listFollowingForApi(
65 serverActor.id,
66 req.query.start,
67 req.query.count,
68 req.query.sort,
69 req.query.search
70 )
65 71
66 return res.json(getFormattedObjects(resultList.data, resultList.total)) 72 return res.json(getFormattedObjects(resultList.data, resultList.total))
67} 73}
68 74
69async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { 75async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
70 const serverActor = await getServerActor() 76 const serverActor = await getServerActor()
71 const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 77 const resultList = await ActorFollowModel.listFollowersForApi(
78 serverActor.id,
79 req.query.start,
80 req.query.count,
81 req.query.sort,
82 req.query.search
83 )
72 84
73 return res.json(getFormattedObjects(resultList.data, resultList.total)) 85 return res.json(getFormattedObjects(resultList.data, resultList.total))
74} 86}
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts
index 43bca2c10..c08192a8c 100644
--- a/server/controllers/api/server/index.ts
+++ b/server/controllers/api/server/index.ts
@@ -2,12 +2,14 @@ import * as express from 'express'
2import { serverFollowsRouter } from './follows' 2import { serverFollowsRouter } from './follows'
3import { statsRouter } from './stats' 3import { statsRouter } from './stats'
4import { serverRedundancyRouter } from './redundancy' 4import { serverRedundancyRouter } from './redundancy'
5import { serverBlocklistRouter } from './server-blocklist'
5 6
6const serverRouter = express.Router() 7const serverRouter = express.Router()
7 8
8serverRouter.use('/', serverFollowsRouter) 9serverRouter.use('/', serverFollowsRouter)
9serverRouter.use('/', serverRedundancyRouter) 10serverRouter.use('/', serverRedundancyRouter)
10serverRouter.use('/', statsRouter) 11serverRouter.use('/', statsRouter)
12serverRouter.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 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 ensureUserHasRight,
9 paginationValidator,
10 setDefaultPagination,
11 setDefaultSort
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockAccountByServerValidator,
19 unblockServerByServerValidator
20} from '../../../middlewares/validators'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26import { UserRight } from '../../../../shared/models/users'
27
28const serverBlocklistRouter = express.Router()
29
30serverBlocklistRouter.get('/blocklist/accounts',
31 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
33 paginationValidator,
34 accountsBlocklistSortValidator,
35 setDefaultSort,
36 setDefaultPagination,
37 asyncMiddleware(listBlockedAccounts)
38)
39
40serverBlocklistRouter.post('/blocklist/accounts',
41 authenticate,
42 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
43 asyncMiddleware(blockAccountValidator),
44 asyncRetryTransactionMiddleware(blockAccount)
45)
46
47serverBlocklistRouter.delete('/blocklist/accounts/:accountName',
48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
50 asyncMiddleware(unblockAccountByServerValidator),
51 asyncRetryTransactionMiddleware(unblockAccount)
52)
53
54serverBlocklistRouter.get('/blocklist/servers',
55 authenticate,
56 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
57 paginationValidator,
58 serversBlocklistSortValidator,
59 setDefaultSort,
60 setDefaultPagination,
61 asyncMiddleware(listBlockedServers)
62)
63
64serverBlocklistRouter.post('/blocklist/servers',
65 authenticate,
66 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
67 asyncMiddleware(blockServerValidator),
68 asyncRetryTransactionMiddleware(blockServer)
69)
70
71serverBlocklistRouter.delete('/blocklist/servers/:host',
72 authenticate,
73 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
74 asyncMiddleware(unblockServerByServerValidator),
75 asyncRetryTransactionMiddleware(unblockServer)
76)
77
78export {
79 serverBlocklistRouter
80}
81
82// ---------------------------------------------------------------------------
83
84async 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
92async 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
101async 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
109async 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
117async 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
126async function unblockServer (req: express.Request, res: express.Response) {
127 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
128
129 await removeServerFromBlocklist(serverBlock)
130
131 return res.status(204).end()
132}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 0b0081520..9fcb8077f 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user'
37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
38import { meRouter } from './me' 38import { meRouter } from './me'
39import { deleteUserToken } from '../../../lib/oauth-model' 39import { deleteUserToken } from '../../../lib/oauth-model'
40import { myBlocklistRouter } from './my-blocklist'
40 41
41const auditLogger = auditLoggerFactory('users') 42const auditLogger = auditLoggerFactory('users')
42 43
@@ -53,6 +54,7 @@ const askSendEmailLimiter = new RateLimit({
53}) 54})
54 55
55const usersRouter = express.Router() 56const usersRouter = express.Router()
57usersRouter.use('/', myBlocklistRouter)
56usersRouter.use('/', meRouter) 58usersRouter.use('/', meRouter)
57 59
58usersRouter.get('/autocomplete', 60usersRouter.get('/autocomplete',
@@ -238,7 +240,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response, n
238} 240}
239 241
240async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 242async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) {
241 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) 243 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
242 244
243 return res.json(getFormattedObjects(resultList.data, resultList.total)) 245 return res.json(getFormattedObjects(resultList.data, resultList.total))
244} 246}
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 591ec6b25..82299747d 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -238,7 +238,8 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
238 nsfw: buildNSFWFilter(res, req.query.nsfw), 238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter, 239 filter: req.query.filter as VideoFilter,
240 withFiles: false, 240 withFiles: false,
241 actorId: user.Account.Actor.id 241 actorId: user.Account.Actor.id,
242 user
242 }) 243 })
243 244
244 return res.json(getFormattedObjects(resultList.data, resultList.total)) 245 return res.json(getFormattedObjects(resultList.data, resultList.total))
@@ -327,6 +328,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
327 if (body.password !== undefined) user.password = body.password 328 if (body.password !== undefined) user.password = body.password
328 if (body.email !== undefined) user.email = body.email 329 if (body.email !== undefined) user.email = body.email
329 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy 330 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
331 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
330 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 332 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
331 333
332 await sequelizeTypescript.transaction(async t => { 334 await sequelizeTypescript.transaction(async t => {
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
new file mode 100644
index 000000000..9575eab46
--- /dev/null
+++ b/server/controllers/api/users/my-blocklist.ts
@@ -0,0 +1,125 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 paginationValidator,
9 setDefaultPagination,
10 setDefaultSort,
11 unblockAccountByAccountValidator
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockServerByAccountValidator
19} from '../../../middlewares/validators'
20import { UserModel } from '../../../models/account/user'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26
27const myBlocklistRouter = express.Router()
28
29myBlocklistRouter.get('/me/blocklist/accounts',
30 authenticate,
31 paginationValidator,
32 accountsBlocklistSortValidator,
33 setDefaultSort,
34 setDefaultPagination,
35 asyncMiddleware(listBlockedAccounts)
36)
37
38myBlocklistRouter.post('/me/blocklist/accounts',
39 authenticate,
40 asyncMiddleware(blockAccountValidator),
41 asyncRetryTransactionMiddleware(blockAccount)
42)
43
44myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
45 authenticate,
46 asyncMiddleware(unblockAccountByAccountValidator),
47 asyncRetryTransactionMiddleware(unblockAccount)
48)
49
50myBlocklistRouter.get('/me/blocklist/servers',
51 authenticate,
52 paginationValidator,
53 serversBlocklistSortValidator,
54 setDefaultSort,
55 setDefaultPagination,
56 asyncMiddleware(listBlockedServers)
57)
58
59myBlocklistRouter.post('/me/blocklist/servers',
60 authenticate,
61 asyncMiddleware(blockServerValidator),
62 asyncRetryTransactionMiddleware(blockServer)
63)
64
65myBlocklistRouter.delete('/me/blocklist/servers/:host',
66 authenticate,
67 asyncMiddleware(unblockServerByAccountValidator),
68 asyncRetryTransactionMiddleware(unblockServer)
69)
70
71export {
72 myBlocklistRouter
73}
74
75// ---------------------------------------------------------------------------
76
77async 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
85async 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
94async 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
102async 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
110async 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
119async function unblockServer (req: express.Request, res: express.Response) {
120 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
121
122 await removeServerFromBlocklist(serverBlock)
123
124 return res.status(204).end()
125}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 1fa842d9c..9bf3c5fd8 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -215,9 +215,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
215 languageOneOf: req.query.languageOneOf, 215 languageOneOf: req.query.languageOneOf,
216 tagsOneOf: req.query.tagsOneOf, 216 tagsOneOf: req.query.tagsOneOf,
217 tagsAllOf: req.query.tagsAllOf, 217 tagsAllOf: req.query.tagsAllOf,
218 filter: req.query.filter,
218 nsfw: buildNSFWFilter(res, req.query.nsfw), 219 nsfw: buildNSFWFilter(res, req.query.nsfw),
219 withFiles: false, 220 withFiles: false,
220 videoChannelId: videoChannelInstance.id 221 videoChannelId: videoChannelInstance.id,
222 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
221 }) 223 })
222 224
223 return res.json(getFormattedObjects(resultList.data, resultList.total)) 225 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 4cf8de1ef..3ba918189 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -1,10 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4 addVideoCaptionValidator,
5 deleteVideoCaptionValidator,
6 listVideoCaptionsValidator
7} from '../../../middlewares/validators/video-captions'
8import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
9import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
10import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index dc25e1e85..3875c8f79 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -8,19 +8,19 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
8import { 8import {
9 asyncMiddleware, 9 asyncMiddleware,
10 asyncRetryTransactionMiddleware, 10 asyncRetryTransactionMiddleware,
11 authenticate, 11 authenticate, optionalAuthenticate,
12 paginationValidator, 12 paginationValidator,
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort 14 setDefaultSort
15} from '../../../middlewares' 15} from '../../../middlewares'
16import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
17import { 16import {
18 addVideoCommentReplyValidator, 17 addVideoCommentReplyValidator,
19 addVideoCommentThreadValidator, 18 addVideoCommentThreadValidator,
20 listVideoCommentThreadsValidator, 19 listVideoCommentThreadsValidator,
21 listVideoThreadCommentsValidator, 20 listVideoThreadCommentsValidator,
22 removeVideoCommentValidator 21 removeVideoCommentValidator,
23} from '../../../middlewares/validators/video-comments' 22 videoCommentThreadsSortValidator
23} from '../../../middlewares/validators'
24import { VideoModel } from '../../../models/video/video' 24import { VideoModel } from '../../../models/video/video'
25import { VideoCommentModel } from '../../../models/video/video-comment' 25import { VideoCommentModel } from '../../../models/video/video-comment'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
@@ -36,10 +36,12 @@ videoCommentRouter.get('/:videoId/comment-threads',
36 setDefaultSort, 36 setDefaultSort,
37 setDefaultPagination, 37 setDefaultPagination,
38 asyncMiddleware(listVideoCommentThreadsValidator), 38 asyncMiddleware(listVideoCommentThreadsValidator),
39 optionalAuthenticate,
39 asyncMiddleware(listVideoThreads) 40 asyncMiddleware(listVideoThreads)
40) 41)
41videoCommentRouter.get('/:videoId/comment-threads/:threadId', 42videoCommentRouter.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
70async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { 72async 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
86async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { 90async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const video = res.locals.video as VideoModel 91 const video = res.locals.video as VideoModel
92 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
93
88 let resultList: ResultList<VideoCommentModel> 94 let resultList: ResultList<VideoCommentModel>
89 95
90 if (video.commentsEnabled === true) { 96 if (video.commentsEnabled === true) {
91 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id) 97 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user)
92 } else { 98 } else {
93 resultList = { 99 resultList = {
94 total: 0, 100 total: 0,
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 15ef8d458..664154406 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -57,6 +57,7 @@ import { videoCaptionsRouter } from './captions'
57import { videoImportsRouter } from './import' 57import { videoImportsRouter } from './import'
58import { resetSequelizeInstance } from '../../../helpers/database-utils' 58import { resetSequelizeInstance } from '../../../helpers/database-utils'
59import { rename } from 'fs-extra' 59import { rename } from 'fs-extra'
60import { watchingRouter } from './watching'
60 61
61const auditLogger = auditLoggerFactory('videos') 62const auditLogger = auditLoggerFactory('videos')
62const videosRouter = express.Router() 63const videosRouter = express.Router()
@@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter)
86videosRouter.use('/', videoCaptionsRouter) 87videosRouter.use('/', videoCaptionsRouter)
87videosRouter.use('/', videoImportsRouter) 88videosRouter.use('/', videoImportsRouter)
88videosRouter.use('/', ownershipVideoRouter) 89videosRouter.use('/', ownershipVideoRouter)
90videosRouter.use('/', watchingRouter)
89 91
90videosRouter.get('/categories', listVideoCategories) 92videosRouter.get('/categories', listVideoCategories)
91videosRouter.get('/licences', listVideoLicences) 93videosRouter.get('/licences', listVideoLicences)
@@ -119,6 +121,7 @@ videosRouter.get('/:id/description',
119 asyncMiddleware(getVideoDescription) 121 asyncMiddleware(getVideoDescription)
120) 122)
121videosRouter.get('/:id', 123videosRouter.get('/:id',
124 optionalAuthenticate,
122 asyncMiddleware(videosGetValidator), 125 asyncMiddleware(videosGetValidator),
123 getVideo 126 getVideo
124) 127)
@@ -433,7 +436,8 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
433 tagsAllOf: req.query.tagsAllOf, 436 tagsAllOf: req.query.tagsAllOf,
434 nsfw: buildNSFWFilter(res, req.query.nsfw), 437 nsfw: buildNSFWFilter(res, req.query.nsfw),
435 filter: req.query.filter as VideoFilter, 438 filter: req.query.filter as VideoFilter,
436 withFiles: false 439 withFiles: false,
440 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
437 }) 441 })
438 442
439 return res.json(getFormattedObjects(resultList.data, resultList.total)) 443 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
new file mode 100644
index 000000000..e8876b47a
--- /dev/null
+++ b/server/controllers/api/videos/watching.ts
@@ -0,0 +1,36 @@
1import * as express from 'express'
2import { UserWatchingVideo } from '../../../../shared'
3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
4import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
5import { UserModel } from '../../../models/account/user'
6
7const watchingRouter = express.Router()
8
9watchingRouter.put('/:videoId/watching',
10 authenticate,
11 asyncMiddleware(videoWatchingValidator),
12 asyncRetryTransactionMiddleware(userWatchVideo)
13)
14
15// ---------------------------------------------------------------------------
16
17export {
18 watchingRouter
19}
20
21// ---------------------------------------------------------------------------
22
23async function userWatchVideo (req: express.Request, res: express.Response) {
24 const user = res.locals.oauth.token.User as UserModel
25
26 const body: UserWatchingVideo = req.body
27 const { id: videoId } = res.locals.video as { id: number }
28
29 await UserVideoHistoryModel.upsert({
30 videoId,
31 userId: user.id,
32 currentTime: body.currentTime
33 })
34
35 return res.type('json').status(204).end()
36}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index b30ad8e8d..ccb9b6029 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,7 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' 2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants'
3import { THUMBNAILS_SIZE } from '../initializers' 3import { THUMBNAILS_SIZE } from '../initializers'
4import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' 4import {
5 asyncMiddleware,
6 commonVideosFiltersValidator,
7 setDefaultSort,
8 videoCommentsFeedsValidator,
9 videoFeedsValidator,
10 videosSortValidator
11} from '../middlewares'
5import { VideoModel } from '../models/video/video' 12import { VideoModel } from '../models/video/video'
6import * as Feed from 'pfeed' 13import * as Feed from 'pfeed'
7import { AccountModel } from '../models/account/account' 14import { AccountModel } from '../models/account/account'
@@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format',
22 videosSortValidator, 29 videosSortValidator,
23 setDefaultSort, 30 setDefaultSort,
24 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), 31 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
32 commonVideosFiltersValidator,
25 asyncMiddleware(videoFeedsValidator), 33 asyncMiddleware(videoFeedsValidator),
26 asyncMiddleware(generateVideoFeed) 34 asyncMiddleware(generateVideoFeed)
27) 35)
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 224e4fe92..84e33c0e9 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -21,6 +21,7 @@ const timeTable = {
21 week: 3600000 * 24 * 7, 21 week: 3600000 * 24 * 7,
22 month: 3600000 * 24 * 30 22 month: 3600000 * 24 * 30
23} 23}
24
24export function parseDuration (duration: number | string): number { 25export function parseDuration (duration: number | string): number {
25 if (typeof duration === 'number') return duration 26 if (typeof duration === 'number') return duration
26 27
@@ -41,6 +42,53 @@ export function parseDuration (duration: number | string): number {
41 throw new Error('Duration could not be properly parsed') 42 throw new Error('Duration could not be properly parsed')
42} 43}
43 44
45export function parseBytes (value: string | number): number {
46 if (typeof value === 'number') return value
47
48 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
49 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
50 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
51 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
52 const t = /^(\d+)\s*TB$/
53 const g = /^(\d+)\s*GB$/
54 const m = /^(\d+)\s*MB$/
55 const b = /^(\d+)\s*B$/
56 let match
57
58 if (value.match(tgm)) {
59 match = value.match(tgm)
60 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
61 + parseInt(match[2], 10) * 1024 * 1024 * 1024
62 + parseInt(match[3], 10) * 1024 * 1024
63 } else if (value.match(tg)) {
64 match = value.match(tg)
65 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
66 + parseInt(match[2], 10) * 1024 * 1024 * 1024
67 } else if (value.match(tm)) {
68 match = value.match(tm)
69 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
70 + parseInt(match[2], 10) * 1024 * 1024
71 } else if (value.match(gm)) {
72 match = value.match(gm)
73 return parseInt(match[1], 10) * 1024 * 1024 * 1024
74 + parseInt(match[2], 10) * 1024 * 1024
75 } else if (value.match(t)) {
76 match = value.match(t)
77 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
78 } else if (value.match(g)) {
79 match = value.match(g)
80 return parseInt(match[1], 10) * 1024 * 1024 * 1024
81 } else if (value.match(m)) {
82 match = value.match(m)
83 return parseInt(match[1], 10) * 1024 * 1024
84 } else if (value.match(b)) {
85 match = value.match(b)
86 return parseInt(match[1], 10) * 1024
87 } else {
88 return parseInt(value, 10)
89 }
90}
91
44function sanitizeUrl (url: string) { 92function sanitizeUrl (url: string) {
45 const urlObject = new URL(url) 93 const urlObject = new URL(url)
46 94
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index f88d26561..95fe824b9 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -81,19 +81,20 @@ function isRemoteVideoUrlValid (url: any) {
81 81
82 return url.type === 'Link' && 82 return url.type === 'Link' &&
83 ( 83 (
84 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && 84 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
85 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 &&
85 isActivityPubUrlValid(url.href) && 86 isActivityPubUrlValid(url.href) &&
86 validator.isInt(url.height + '', { min: 0 }) && 87 validator.isInt(url.height + '', { min: 0 }) &&
87 validator.isInt(url.size + '', { min: 0 }) && 88 validator.isInt(url.size + '', { min: 0 }) &&
88 (!url.fps || validator.isInt(url.fps + '', { min: -1 })) 89 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
89 ) || 90 ) ||
90 ( 91 (
91 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && 92 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 &&
92 isActivityPubUrlValid(url.href) && 93 isActivityPubUrlValid(url.href) &&
93 validator.isInt(url.height + '', { min: 0 }) 94 validator.isInt(url.height + '', { min: 0 })
94 ) || 95 ) ||
95 ( 96 (
96 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && 97 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
97 validator.isLength(url.href, { min: 5 }) && 98 validator.isLength(url.href, { min: 5 }) &&
98 validator.isInt(url.height + '', { min: 0 }) 99 validator.isInt(url.height + '', { min: 0 })
99 ) 100 )
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 90fc74a48..1cb5e5b0f 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -42,6 +42,10 @@ function isUserNSFWPolicyValid (value: any) {
42 return exists(value) && nsfwPolicies.indexOf(value) !== -1 42 return exists(value) && nsfwPolicies.indexOf(value) !== -1
43} 43}
44 44
45function isUserWebTorrentEnabledValid (value: any) {
46 return isBooleanValid(value)
47}
48
45function isUserAutoPlayVideoValid (value: any) { 49function isUserAutoPlayVideoValid (value: any) {
46 return isBooleanValid(value) 50 return isBooleanValid(value)
47} 51}
@@ -78,6 +82,7 @@ export {
78 isUserUsernameValid, 82 isUserUsernameValid,
79 isUserEmailVerifiedValid, 83 isUserEmailVerifiedValid,
80 isUserNSFWPolicyValid, 84 isUserNSFWPolicyValid,
85 isUserWebTorrentEnabledValid,
81 isUserAutoPlayVideoValid, 86 isUserAutoPlayVideoValid,
82 isUserDisplayNameValid, 87 isUserDisplayNameValid,
83 isUserDescriptionValid, 88 isUserDescriptionValid,
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 9875c68bd..a13b09ac8 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -3,7 +3,7 @@ import 'express-validator'
3import { values } from 'lodash' 3import { values } from 'lodash'
4import 'multer' 4import 'multer'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS,
9 VIDEO_CATEGORIES, 9 VIDEO_CATEGORIES,
@@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video'
22 22
23const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 23const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
24 24
25function isVideoFilterValid (filter: VideoFilter) {
26 return filter === 'local' || filter === 'all-local'
27}
28
25function isVideoCategoryValid (value: any) { 29function isVideoCategoryValid (value: any) {
26 return value === null || VIDEO_CATEGORIES[ value ] !== undefined 30 return value === null || VIDEO_CATEGORIES[ value ] !== undefined
27} 31}
@@ -154,7 +158,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
154} 158}
155 159
156async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { 160async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
157 const video = await fetchVideo(id, fetchType) 161 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
162
163 const video = await fetchVideo(id, fetchType, userId)
158 164
159 if (video === null) { 165 if (video === null) {
160 res.status(404) 166 res.status(404)
@@ -223,5 +229,6 @@ export {
223 isVideoExist, 229 isVideoExist,
224 isVideoImage, 230 isVideoImage,
225 isVideoChannelOfAccountExist, 231 isVideoChannelOfAccountExist,
226 isVideoSupportValid 232 isVideoSupportValid,
233 isVideoFilterValid
227} 234}
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 8a9cee8c5..162fe2244 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { CONFIG, REMOTE_SCHEME } from '../initializers' 3import { CONFIG, REMOTE_SCHEME } from '../initializers'
4import { logger } from './logger' 4import { logger } from './logger'
5import { User } from '../../shared/models/users'
6import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAsync, generateRandomString } from './utils'
7import { extname } from 'path' 6import { extname } from 'path'
8import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
@@ -101,7 +100,7 @@ function createReqFiles (
101} 100}
102 101
103function isUserAbleToSearchRemoteURI (res: express.Response) { 102function isUserAbleToSearchRemoteURI (res: express.Response) {
104 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined 103 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
105 104
106 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || 105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
107 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) 106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 22bc25476..8b9045038 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,6 +1,6 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { join } from 'path'
3import { VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) {
55 return 0 55 return 0
56} 56}
57 57
58async function getVideoFileBitrate (path: string) {
59 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => {
61 if (err) return rej(err)
62
63 return res(metadata.format.bit_rate)
64 })
65 })
66}
67
58function getDurationFromVideoFile (path: string) { 68function getDurationFromVideoFile (path: string) {
59 return new Promise<number>((res, rej) => { 69 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => { 70 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -106,45 +116,50 @@ type TranscodeOptions = {
106 116
107function transcode (options: TranscodeOptions) { 117function transcode (options: TranscodeOptions) {
108 return new Promise<void>(async (res, rej) => { 118 return new Promise<void>(async (res, rej) => {
109 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 119 try {
110 .output(options.outputPath) 120 let fps = await getVideoFileFPS(options.inputPath)
111 .preset(standard)
112
113 if (CONFIG.TRANSCODING.THREADS > 0) {
114 // if we don't set any threads ffmpeg will chose automatically
115 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
116 }
117
118 let fps = await getVideoFileFPS(options.inputPath)
119 if (options.resolution !== undefined) {
120 // '?x720' or '720x?' for example
121 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
122 command = command.size(size)
123
124 // On small/medium resolutions, limit FPS 121 // On small/medium resolutions, limit FPS
125 if ( 122 if (
123 options.resolution !== undefined &&
126 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && 124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
127 fps > VIDEO_TRANSCODING_FPS.AVERAGE 125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
128 ) { 126 ) {
129 fps = VIDEO_TRANSCODING_FPS.AVERAGE 127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
130 } 128 }
131 }
132 129
133 if (fps) { 130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
134 // Hard FPS limits 131 .output(options.outputPath)
135 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX 132 command = await presetH264(command, options.resolution, fps)
136 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
137 133
138 command = command.withFPS(fps) 134 if (CONFIG.TRANSCODING.THREADS > 0) {
139 } 135 // if we don't set any threads ffmpeg will chose automatically
136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
137 }
138
139 if (options.resolution !== undefined) {
140 // '?x720' or '720x?' for example
141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
142 command = command.size(size)
143 }
144
145 if (fps) {
146 // Hard FPS limits
147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
140 149
141 command 150 command = command.withFPS(fps)
142 .on('error', (err, stdout, stderr) => { 151 }
143 logger.error('Error in transcoding job.', { stdout, stderr }) 152
144 return rej(err) 153 command
145 }) 154 .on('error', (err, stdout, stderr) => {
146 .on('end', res) 155 logger.error('Error in transcoding job.', { stdout, stderr })
147 .run() 156 return rej(err)
157 })
158 .on('end', res)
159 .run()
160 } catch (err) {
161 return rej(err)
162 }
148 }) 163 })
149} 164}
150 165
@@ -157,7 +172,8 @@ export {
157 transcode, 172 transcode,
158 getVideoFileFPS, 173 getVideoFileFPS,
159 computeResolutionsToTranscode, 174 computeResolutionsToTranscode,
160 audio 175 audio,
176 getVideoFileBitrate
161} 177}
162 178
163// --------------------------------------------------------------------------- 179// ---------------------------------------------------------------------------
@@ -182,11 +198,10 @@ function getVideoFileStream (path: string) {
182 * and quality. Superfast and ultrafast will give you better 198 * and quality. Superfast and ultrafast will give you better
183 * performance, but then quality is noticeably worse. 199 * performance, but then quality is noticeably worse.
184 */ 200 */
185function veryfast (_ffmpeg) { 201async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
186 _ffmpeg 202 let localCommand = await presetH264(command, resolution, fps)
187 .preset(standard) 203 localCommand = localCommand.outputOption('-preset:v veryfast')
188 .outputOption('-preset:v veryfast') 204 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
189 .outputOption(['--aq-mode=2', '--aq-strength=1.3'])
190 /* 205 /*
191 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 206 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
192 Our target situation is closer to a livestream than a stream, 207 Our target situation is closer to a livestream than a stream,
@@ -198,31 +213,39 @@ function veryfast (_ffmpeg) {
198 Make up for most of the loss of grain and macroblocking 213 Make up for most of the loss of grain and macroblocking
199 with less computing power. 214 with less computing power.
200 */ 215 */
216
217 return localCommand
201} 218}
202 219
203/** 220/**
204 * A preset optimised for a stillimage audio video 221 * A preset optimised for a stillimage audio video
205 */ 222 */
206function audio (_ffmpeg) { 223async function presetStillImageWithAudio (
207 _ffmpeg 224 command: ffmpeg.FfmpegCommand,
208 .preset(veryfast) 225 resolution: VideoResolution,
209 .outputOption('-tune stillimage') 226 fps: number
227): Promise<ffmpeg.FfmpegCommand> {
228 let localCommand = await presetH264VeryFast(command, resolution, fps)
229 localCommand = localCommand.outputOption('-tune stillimage')
230
231 return localCommand
210} 232}
211 233
212/** 234/**
213 * A toolbox to play with audio 235 * A toolbox to play with audio
214 */ 236 */
215namespace audio { 237namespace audio {
216 export const get = (_ffmpeg, pos: number | string = 0) => { 238 export const get = (option: ffmpeg.FfmpegCommand | string) => {
217 // without position, ffprobe considers the last input only 239 // without position, ffprobe considers the last input only
218 // we make it consider the first input only 240 // we make it consider the first input only
219 // if you pass a file path to pos, then ffprobe acts on that file directly 241 // if you pass a file path to pos, then ffprobe acts on that file directly
220 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { 242 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
221 _ffmpeg.ffprobe(pos, (err,data) => { 243
244 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
222 if (err) return rej(err) 245 if (err) return rej(err)
223 246
224 if ('streams' in data) { 247 if ('streams' in data) {
225 const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') 248 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
226 if (audioStream) { 249 if (audioStream) {
227 return res({ 250 return res({
228 absolutePath: data.format.filename, 251 absolutePath: data.format.filename,
@@ -230,8 +253,15 @@ namespace audio {
230 }) 253 })
231 } 254 }
232 } 255 }
256
233 return res({ absolutePath: data.format.filename }) 257 return res({ absolutePath: data.format.filename })
234 }) 258 }
259
260 if (typeof option === 'string') {
261 return ffmpeg.ffprobe(option, parseFfprobe)
262 }
263
264 return option.ffprobe(parseFfprobe)
235 }) 265 })
236 } 266 }
237 267
@@ -273,39 +303,48 @@ namespace audio {
273 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 303 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
274 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 304 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
275 */ 305 */
276async function standard (_ffmpeg) { 306async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
277 let localFfmpeg = _ffmpeg 307 let localCommand = command
278 .format('mp4') 308 .format('mp4')
279 .videoCodec('libx264') 309 .videoCodec('libx264')
280 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 310 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
281 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it 311 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it
282 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 312 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
313 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
283 .outputOption('-map_metadata -1') // strip all metadata 314 .outputOption('-map_metadata -1') // strip all metadata
284 .outputOption('-movflags faststart') 315 .outputOption('-movflags faststart')
285 const _audio = await audio.get(localFfmpeg)
286 316
287 if (!_audio.audioStream) { 317 const parsedAudio = await audio.get(localCommand)
288 return localFfmpeg.noAudio()
289 }
290 318
291 // we favor VBR, if a good AAC encoder is available 319 if (!parsedAudio.audioStream) {
292 if ((await checkFFmpegEncoders()).get('libfdk_aac')) { 320 localCommand = localCommand.noAudio()
293 return localFfmpeg 321 } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
322 localCommand = localCommand
294 .audioCodec('libfdk_aac') 323 .audioCodec('libfdk_aac')
295 .audioQuality(5) 324 .audioQuality(5)
325 } else {
326 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
327 // of course this is far from perfect, but it might save some space in the end
328 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
329 let bitrate: number
330 if (audio.bitrate[ audioCodecName ]) {
331 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
332
333 if (bitrate === -1) localCommand = localCommand.audioCodec('copy')
334 else if (bitrate !== undefined) localCommand = localCommand.audioBitrate(bitrate)
335 }
296 } 336 }
297 337
298 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 338 // Constrained Encoding (VBV)
299 // of course this is far from perfect, but it might save some space in the end 339 // https://slhck.info/video/2017/03/01/rate-control.html
300 const audioCodecName = _audio.audioStream['codec_name'] 340 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
301 let bitrate: number 341 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
302 if (audio.bitrate[audioCodecName]) { 342 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
303 bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate'])
304
305 if (bitrate === -1) return localFfmpeg.audioCodec('copy')
306 }
307 343
308 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) 344 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
345 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
346 // https://superuser.com/a/908325
347 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
309 348
310 return localFfmpeg 349 return localCommand
311} 350}
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index cb5f27240..8ef7b1359 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -62,10 +62,7 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any)
62 62
63 return jsig.promises 63 return jsig.promises
64 .verify(signedDocument, options) 64 .verify(signedDocument, options)
65 .then((result: { verified: boolean }) => { 65 .then((result: { verified: boolean }) => result.verified)
66 logger.info('coucou', result)
67 return result.verified
68 })
69 .catch(err => { 66 .catch(err => {
70 logger.error('Cannot check signature.', { err }) 67 logger.error('Cannot check signature.', { err })
71 return false 68 return false
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 6228fec04..049c3f8bc 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () {
40 const application = await ApplicationModel.load() 40 const application = await ApplicationModel.load()
41 if (!application) throw Error('Could not load Application from database.') 41 if (!application) throw Error('Could not load Application from database.')
42 42
43 return application.Account.Actor 43 const actor = application.Account.Actor
44 actor.Account = application.Account
45
46 return actor
44}) 47})
45 48
46function generateVideoTmpPath (target: string | ParseTorrent) { 49function generateVideoTmpPath (target: string | ParseTorrent) {
@@ -77,6 +80,20 @@ async function getVersion () {
77 return require('../../../package.json').version 80 return require('../../../package.json').version
78} 81}
79 82
83/**
84 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
85 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
86 * not contain a UUID, returns null.
87 */
88function getUUIDFromFilename (filename: string) {
89 const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
90 const result = filename.match(regex)
91
92 if (!result || Array.isArray(result) === false) return null
93
94 return result[0]
95}
96
80// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
81 98
82export { 99export {
@@ -86,5 +103,6 @@ export {
86 getSecureTorrentName, 103 getSecureTorrentName,
87 getServerActor, 104 getServerActor,
88 getVersion, 105 getVersion,
89 generateVideoTmpPath 106 generateVideoTmpPath,
107 getUUIDFromFilename
90} 108}
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index b1577a6b0..1bd21467d 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video') return VideoModel.load(id) 8 if (fetchType === 'only-video') return VideoModel.load(id)
9 9
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 4f46d406a..9dfb5d68c 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -77,7 +77,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
77 } 77 }
78 } 78 }
79 79
80 checkFFmpegEncoders() 80 return checkFFmpegEncoders()
81} 81}
82 82
83// Optional encoders, if present, can be used to improve transcoding 83// Optional encoders, if present, can be used to improve transcoding
@@ -95,10 +95,10 @@ async function checkFFmpegEncoders (): Promise<Map<string, boolean>> {
95 supportedOptionalEncoders = new Map<string, boolean>() 95 supportedOptionalEncoders = new Map<string, boolean>()
96 96
97 for (const encoder of optionalEncoders) { 97 for (const encoder of optionalEncoders) {
98 supportedOptionalEncoders.set(encoder, 98 supportedOptionalEncoders.set(encoder, encoders[encoder] !== undefined)
99 encoders[encoder] !== undefined
100 )
101 } 99 }
100
101 return supportedOptionalEncoders
102} 102}
103 103
104// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index fd2308eb6..28d51068b 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -3,9 +3,9 @@ import { dirname, join } from 'path'
3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' 3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' 6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 7// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, parseDuration, parseBytes, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
10import { invert } from 'lodash' 10import { invert } from 'lodash'
11import { CronRepeatOptions, EveryRepeatOptions } from 'bull' 11import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
@@ -16,7 +16,7 @@ let config: IConfig = require('config')
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
18 18
19const LAST_MIGRATION_VERSION = 275 19const LAST_MIGRATION_VERSION = 285
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
@@ -47,7 +47,10 @@ const SORTABLE_COLUMNS = {
47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], 47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ],
48 48
49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], 49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ],
50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] 50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
51
52 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
53 SERVERS_BLOCKLIST: [ 'createdAt' ]
51} 54}
52 55
53const OAUTH_LIFETIME = { 56const OAUTH_LIFETIME = {
@@ -232,8 +235,8 @@ const CONFIG = {
232 } 235 }
233 }, 236 },
234 USER: { 237 USER: {
235 get VIDEO_QUOTA () { return config.get<number>('user.video_quota') }, 238 get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) },
236 get VIDEO_QUOTA_DAILY () { return config.get<number>('user.video_quota_daily') } 239 get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) }
237 }, 240 },
238 TRANSCODING: { 241 TRANSCODING: {
239 get ENABLED () { return config.get<boolean>('transcoding.enabled') }, 242 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
@@ -292,7 +295,7 @@ const CONFIG = {
292const CONSTRAINTS_FIELDS = { 295const CONSTRAINTS_FIELDS = {
293 USERS: { 296 USERS: {
294 NAME: { min: 3, max: 120 }, // Length 297 NAME: { min: 3, max: 120 }, // Length
295 DESCRIPTION: { min: 3, max: 250 }, // Length 298 DESCRIPTION: { min: 3, max: 1000 }, // Length
296 USERNAME: { min: 3, max: 20 }, // Length 299 USERNAME: { min: 3, max: 20 }, // Length
297 PASSWORD: { min: 6, max: 255 }, // Length 300 PASSWORD: { min: 6, max: 255 }, // Length
298 VIDEO_QUOTA: { min: -1 }, 301 VIDEO_QUOTA: { min: -1 },
@@ -308,8 +311,8 @@ const CONSTRAINTS_FIELDS = {
308 }, 311 },
309 VIDEO_CHANNELS: { 312 VIDEO_CHANNELS: {
310 NAME: { min: 3, max: 120 }, // Length 313 NAME: { min: 3, max: 120 }, // Length
311 DESCRIPTION: { min: 3, max: 500 }, // Length 314 DESCRIPTION: { min: 3, max: 1000 }, // Length
312 SUPPORT: { min: 3, max: 500 }, // Length 315 SUPPORT: { min: 3, max: 1000 }, // Length
313 URL: { min: 3, max: 2000 } // Length 316 URL: { min: 3, max: 2000 } // Length
314 }, 317 },
315 VIDEO_CAPTIONS: { 318 VIDEO_CAPTIONS: {
@@ -338,7 +341,7 @@ const CONSTRAINTS_FIELDS = {
338 LANGUAGE: { min: 1, max: 10 }, // Length 341 LANGUAGE: { min: 1, max: 10 }, // Length
339 TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length 342 TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
340 DESCRIPTION: { min: 3, max: 10000 }, // Length 343 DESCRIPTION: { min: 3, max: 10000 }, // Length
341 SUPPORT: { min: 3, max: 500 }, // Length 344 SUPPORT: { min: 3, max: 1000 }, // Length
342 IMAGE: { 345 IMAGE: {
343 EXTNAME: [ '.jpg', '.jpeg' ], 346 EXTNAME: [ '.jpg', '.jpeg' ],
344 FILE_SIZE: { 347 FILE_SIZE: {
@@ -393,7 +396,7 @@ const RATES_LIMIT = {
393} 396}
394 397
395let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 398let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
396const VIDEO_TRANSCODING_FPS = { 399const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
397 MIN: 10, 400 MIN: 10,
398 AVERAGE: 30, 401 AVERAGE: 30,
399 MAX: 60, 402 MAX: 60,
@@ -421,7 +424,7 @@ const VIDEO_CATEGORIES = {
421 8: 'People', 424 8: 'People',
422 9: 'Comedy', 425 9: 'Comedy',
423 10: 'Entertainment', 426 10: 'Entertainment',
424 11: 'News', 427 11: 'News & Politics',
425 12: 'How To', 428 12: 'How To',
426 13: 'Education', 429 13: 'Education',
427 14: 'Activism', 430 14: 'Activism',
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 4d57bf8aa..dd5b9bf67 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -28,6 +28,9 @@ import { VideoImportModel } from '../models/video/video-import'
28import { VideoViewModel } from '../models/video/video-views' 28import { VideoViewModel } from '../models/video/video-views'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' 29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist'
31 34
32require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 35require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
33 36
@@ -89,7 +92,10 @@ async function initDatabaseModels (silent: boolean) {
89 ScheduleVideoUpdateModel, 92 ScheduleVideoUpdateModel,
90 VideoImportModel, 93 VideoImportModel,
91 VideoViewModel, 94 VideoViewModel,
92 VideoRedundancyModel 95 VideoRedundancyModel,
96 UserVideoHistoryModel,
97 AccountBlocklistModel,
98 ServerBlocklistModel
93 ]) 99 ])
94 100
95 // Check extensions exist in the database 101 // Check extensions exist in the database
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts
index 63f3984dd..6d253f04f 100644
--- a/server/initializers/migrations/0120-video-null.ts
+++ b/server/initializers/migrations/0120-video-null.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction,
@@ -28,7 +27,7 @@ async function up (utils: {
28 27
29 { 28 {
30 const data = { 29 const data = {
31 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), 30 type: Sequelize.STRING(10000),
32 allowNull: true, 31 allowNull: true,
33 defaultValue: null 32 defaultValue: null
34 } 33 }
diff --git a/server/initializers/migrations/0195-support.ts b/server/initializers/migrations/0195-support.ts
index 8722a5f22..3b9eabe79 100644
--- a/server/initializers/migrations/0195-support.ts
+++ b/server/initializers/migrations/0195-support.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../index'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction,
@@ -8,7 +7,7 @@ async function up (utils: {
8}): Promise<void> { 7}): Promise<void> {
9 { 8 {
10 const data = { 9 const data = {
11 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max), 10 type: Sequelize.STRING(500),
12 allowNull: true, 11 allowNull: true,
13 defaultValue: null 12 defaultValue: null
14 } 13 }
@@ -17,7 +16,7 @@ async function up (utils: {
17 16
18 { 17 {
19 const data = { 18 const data = {
20 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max), 19 type: Sequelize.STRING(500),
21 allowNull: true, 20 allowNull: true,
22 defaultValue: null 21 defaultValue: null
23 } 22 }
@@ -26,7 +25,7 @@ async function up (utils: {
26 25
27 { 26 {
28 const data = { 27 const data = {
29 type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max), 28 type: Sequelize.STRING(250),
30 allowNull: true, 29 allowNull: true,
31 defaultValue: null 30 defaultValue: null
32 } 31 }
@@ -35,7 +34,7 @@ async function up (utils: {
35 34
36 { 35 {
37 const data = { 36 const data = {
38 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), 37 type: Sequelize.STRING(10000),
39 allowNull: true, 38 allowNull: true,
40 defaultValue: null 39 defaultValue: null
41 } 40 }
diff --git a/server/initializers/migrations/0245-user-blocked.ts b/server/initializers/migrations/0245-user-blocked.ts
index 5a04ecd2b..19c7d5b9c 100644
--- a/server/initializers/migrations/0245-user-blocked.ts
+++ b/server/initializers/migrations/0245-user-blocked.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction 4 transaction: Sequelize.Transaction
@@ -31,7 +30,7 @@ async function up (utils: {
31 30
32 { 31 {
33 const data = { 32 const data = {
34 type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max), 33 type: Sequelize.STRING(250),
35 allowNull: true, 34 allowNull: true,
36 defaultValue: null 35 defaultValue: null
37 } 36 }
diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts
index acb668ae1..50de25182 100644
--- a/server/initializers/migrations/0250-video-abuse-state.ts
+++ b/server/initializers/migrations/0250-video-abuse-state.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3import { VideoAbuseState } from '../../../shared/models/videos' 2import { VideoAbuseState } from '../../../shared/models/videos'
4 3
5async function up (utils: { 4async function up (utils: {
@@ -32,7 +31,7 @@ async function up (utils: {
32 31
33 { 32 {
34 const data = { 33 const data = {
35 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max), 34 type: Sequelize.STRING(300),
36 allowNull: true, 35 allowNull: true,
37 defaultValue: null 36 defaultValue: null
38 } 37 }
diff --git a/server/initializers/migrations/0255-video-blacklist-reason.ts b/server/initializers/migrations/0255-video-blacklist-reason.ts
index a380e620e..69d6efb9e 100644
--- a/server/initializers/migrations/0255-video-blacklist-reason.ts
+++ b/server/initializers/migrations/0255-video-blacklist-reason.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3import { VideoAbuseState } from '../../../shared/models/videos' 2import { VideoAbuseState } from '../../../shared/models/videos'
4 3
5async function up (utils: { 4async function up (utils: {
@@ -10,7 +9,7 @@ async function up (utils: {
10 9
11 { 10 {
12 const data = { 11 const data = {
13 type: Sequelize.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max), 12 type: Sequelize.STRING(300),
14 allowNull: true, 13 allowNull: true,
15 defaultValue: null 14 defaultValue: null
16 } 15 }
diff --git a/server/initializers/migrations/0260-upload-quota-daily.ts b/server/initializers/migrations/0260-upload-quota-daily.ts
index d25154ba6..cbbe391ef 100644
--- a/server/initializers/migrations/0260-upload-quota-daily.ts
+++ b/server/initializers/migrations/0260-upload-quota-daily.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction 4 transaction: Sequelize.Transaction
diff --git a/server/initializers/migrations/0280-webtorrent-policy-user.ts b/server/initializers/migrations/0280-webtorrent-policy-user.ts
new file mode 100644
index 000000000..e6488356a
--- /dev/null
+++ b/server/initializers/migrations/0280-webtorrent-policy-user.ts
@@ -0,0 +1,28 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<any> {
8 {
9 const data = {
10 type: Sequelize.BOOLEAN,
11 allowNull: false,
12 defaultValue: true
13 }
14
15 await utils.queryInterface.addColumn('user', 'webTorrentEnabled', data)
16 }
17
18}
19
20async function down (utils: {
21 transaction: Sequelize.Transaction
22 queryInterface: Sequelize.QueryInterface
23 sequelize: Sequelize.Sequelize
24}): Promise<any> {
25 await utils.queryInterface.removeColumn('user', 'webTorrentEnabled')
26}
27
28export { up, down }
diff --git a/server/initializers/migrations/0285-description-support.ts b/server/initializers/migrations/0285-description-support.ts
new file mode 100644
index 000000000..85ef4ef39
--- /dev/null
+++ b/server/initializers/migrations/0285-description-support.ts
@@ -0,0 +1,53 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.STRING(1000),
12 allowNull: true,
13 defaultValue: null
14 }
15 await utils.queryInterface.changeColumn('video', 'support', data)
16 }
17
18 {
19 const data = {
20 type: Sequelize.STRING(1000),
21 allowNull: true,
22 defaultValue: null
23 }
24 await utils.queryInterface.changeColumn('videoChannel', 'support', data)
25 }
26
27 {
28 const data = {
29 type: Sequelize.STRING(1000),
30 allowNull: true,
31 defaultValue: null
32 }
33 await utils.queryInterface.changeColumn('videoChannel', 'description', data)
34 }
35
36 {
37 const data = {
38 type: Sequelize.STRING(1000),
39 allowNull: true,
40 defaultValue: null
41 }
42 await utils.queryInterface.changeColumn('account', 'description', data)
43 }
44}
45
46function down (options) {
47 throw new Error('Not implemented.')
48}
49
50export {
51 up,
52 down
53}
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 55912341c..db9ce3293 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,7 +1,7 @@
1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' 1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import Bluebird = require('bluebird') 4import * as Bluebird from 'bluebird'
5 5
6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
7 logger.info('Crawling ActivityPub data on %s.', uri) 7 logger.info('Crawling ActivityPub data on %s.', uri)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 54cea542f..3da363c0a 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -310,7 +310,8 @@ export {
310function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 310function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
311 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) 311 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
312 312
313 return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') 313 const urlMediaType = url.mediaType || url.mimeType
314 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
314} 315}
315 316
316async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 317async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
@@ -468,7 +469,8 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
468 for (const fileUrl of fileUrls) { 469 for (const fileUrl of fileUrls) {
469 // Fetch associated magnet uri 470 // Fetch associated magnet uri
470 const magnet = videoObject.url.find(u => { 471 const magnet = videoObject.url.find(u => {
471 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height 472 const mediaType = u.mediaType || u.mimeType
473 return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
472 }) 474 })
473 475
474 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) 476 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
@@ -478,8 +480,9 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
478 throw new Error('Cannot parse magnet URI ' + magnet.href) 480 throw new Error('Cannot parse magnet URI ' + magnet.href)
479 } 481 }
480 482
483 const mediaType = fileUrl.mediaType || fileUrl.mimeType
481 const attribute = { 484 const attribute = {
482 extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], 485 extname: VIDEO_MIMETYPE_EXT[ mediaType ],
483 infoHash: parsed.infoHash, 486 infoHash: parsed.infoHash,
484 resolution: fileUrl.height, 487 resolution: fileUrl.height,
485 size: fileUrl.size, 488 size: fileUrl.size,
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
new file mode 100644
index 000000000..1633e500c
--- /dev/null
+++ b/server/lib/blocklist.ts
@@ -0,0 +1,40 @@
1import { sequelizeTypescript } from '../initializers'
2import { AccountBlocklistModel } from '../models/account/account-blocklist'
3import { ServerBlocklistModel } from '../models/server/server-blocklist'
4
5function 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
14function 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
23function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) {
24 return sequelizeTypescript.transaction(async t => {
25 return accountBlock.destroy({ transaction: t })
26 })
27}
28
29function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) {
30 return sequelizeTypescript.transaction(async t => {
31 return serverBlock.destroy({ transaction: t })
32 })
33}
34
35export {
36 addAccountInBlocklist,
37 addServerInBlocklist,
38 removeAccountFromBlocklist,
39 removeServerFromBlocklist
40}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index fc013e0c3..006b25bfd 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -116,7 +116,7 @@ export class ClientHtml {
116 116
117 'og:video:url': embedUrl, 117 'og:video:url': embedUrl,
118 'og:video:secure_url': embedUrl, 118 'og:video:secure_url': embedUrl,
119 'og:video:type': 'text/html', 119 'og:video:type': 'video/mp4',
120 'og:video:width': EMBED_SIZE.width, 120 'og:video:width': EMBED_SIZE.width,
121 'og:video:height': EMBED_SIZE.height, 121 'og:video:height': EMBED_SIZE.height,
122 122
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 1463c93fc..adc0a2a15 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -8,7 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' 11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding'
12 12
13export type VideoFilePayload = { 13export type VideoFilePayload = {
14 videoUUID: string 14 videoUUID: string
@@ -56,7 +56,7 @@ async function processVideoFile (job: Bull.Job) {
56 56
57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) 57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
58 } else { 58 } else {
59 await optimizeOriginalVideofile(video) 59 await optimizeVideofile(video)
60 60
61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) 61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
62 } 62 }
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index e4e435659..abd75d512 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -48,6 +48,8 @@ class Redis {
48 ) 48 )
49 } 49 }
50 50
51 /************* Forgot password *************/
52
51 async setResetPasswordVerificationString (userId: number) { 53 async setResetPasswordVerificationString (userId: number) {
52 const generatedString = await generateRandomString(32) 54 const generatedString = await generateRandomString(32)
53 55
@@ -60,6 +62,8 @@ class Redis {
60 return this.getValue(this.generateResetPasswordKey(userId)) 62 return this.getValue(this.generateResetPasswordKey(userId))
61 } 63 }
62 64
65 /************* Email verification *************/
66
63 async setVerifyEmailVerificationString (userId: number) { 67 async setVerifyEmailVerificationString (userId: number) {
64 const generatedString = await generateRandomString(32) 68 const generatedString = await generateRandomString(32)
65 69
@@ -72,16 +76,20 @@ class Redis {
72 return this.getValue(this.generateVerifyEmailKey(userId)) 76 return this.getValue(this.generateVerifyEmailKey(userId))
73 } 77 }
74 78
79 /************* Views per IP *************/
80
75 setIPVideoView (ip: string, videoUUID: string) { 81 setIPVideoView (ip: string, videoUUID: string) {
76 return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) 82 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
77 } 83 }
78 84
79 async isVideoIPViewExists (ip: string, videoUUID: string) { 85 async isVideoIPViewExists (ip: string, videoUUID: string) {
80 return this.exists(this.buildViewKey(ip, videoUUID)) 86 return this.exists(this.generateViewKey(ip, videoUUID))
81 } 87 }
82 88
89 /************* API cache *************/
90
83 async getCachedRoute (req: express.Request) { 91 async getCachedRoute (req: express.Request) {
84 const cached = await this.getObject(this.buildCachedRouteKey(req)) 92 const cached = await this.getObject(this.generateCachedRouteKey(req))
85 93
86 return cached as CachedRoute 94 return cached as CachedRoute
87 } 95 }
@@ -94,9 +102,11 @@ class Redis {
94 (statusCode) ? { statusCode: statusCode.toString() } : null 102 (statusCode) ? { statusCode: statusCode.toString() } : null
95 ) 103 )
96 104
97 return this.setObject(this.buildCachedRouteKey(req), cached, lifetime) 105 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
98 } 106 }
99 107
108 /************* Video views *************/
109
100 addVideoView (videoId: number) { 110 addVideoView (videoId: number) {
101 const keyIncr = this.generateVideoViewKey(videoId) 111 const keyIncr = this.generateVideoViewKey(videoId)
102 const keySet = this.generateVideosViewKey() 112 const keySet = this.generateVideosViewKey()
@@ -131,33 +141,37 @@ class Redis {
131 ]) 141 ])
132 } 142 }
133 143
134 generateVideosViewKey (hour?: number) { 144 /************* Keys generation *************/
145
146 generateCachedRouteKey (req: express.Request) {
147 return req.method + '-' + req.originalUrl
148 }
149
150 private generateVideosViewKey (hour?: number) {
135 if (!hour) hour = new Date().getHours() 151 if (!hour) hour = new Date().getHours()
136 152
137 return `videos-view-h${hour}` 153 return `videos-view-h${hour}`
138 } 154 }
139 155
140 generateVideoViewKey (videoId: number, hour?: number) { 156 private generateVideoViewKey (videoId: number, hour?: number) {
141 if (!hour) hour = new Date().getHours() 157 if (!hour) hour = new Date().getHours()
142 158
143 return `video-view-${videoId}-h${hour}` 159 return `video-view-${videoId}-h${hour}`
144 } 160 }
145 161
146 generateResetPasswordKey (userId: number) { 162 private generateResetPasswordKey (userId: number) {
147 return 'reset-password-' + userId 163 return 'reset-password-' + userId
148 } 164 }
149 165
150 generateVerifyEmailKey (userId: number) { 166 private generateVerifyEmailKey (userId: number) {
151 return 'verify-email-' + userId 167 return 'verify-email-' + userId
152 } 168 }
153 169
154 buildViewKey (ip: string, videoUUID: string) { 170 private generateViewKey (ip: string, videoUUID: string) {
155 return videoUUID + '-' + ip 171 return videoUUID + '-' + ip
156 } 172 }
157 173
158 buildCachedRouteKey (req: express.Request) { 174 /************* Redis helpers *************/
159 return req.method + '-' + req.originalUrl
160 }
161 175
162 private getValue (key: string) { 176 private getValue (key: string) {
163 return new Promise<string>((res, rej) => { 177 return new Promise<string>((res, rej) => {
@@ -197,6 +211,12 @@ class Redis {
197 }) 211 })
198 } 212 }
199 213
214 private deleteFieldInHash (key: string, field: string) {
215 return new Promise<void>((res, rej) => {
216 this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
217 })
218 }
219
200 private setValue (key: string, value: string, expirationMilliseconds: number) { 220 private setValue (key: string, value: string, expirationMilliseconds: number) {
201 return new Promise<void>((res, rej) => { 221 return new Promise<void>((res, rej) => {
202 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { 222 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
@@ -235,6 +255,16 @@ class Redis {
235 }) 255 })
236 } 256 }
237 257
258 private setValueInHash (key: string, field: string, value: string) {
259 return new Promise<void>((res, rej) => {
260 this.client.hset(this.prefix + key, field, value, (err) => {
261 if (err) return rej(err)
262
263 return res()
264 })
265 })
266 }
267
238 private increment (key: string) { 268 private increment (key: string) {
239 return new Promise<number>((res, rej) => { 269 return new Promise<number>((res, rej) => {
240 this.client.incr(this.prefix + key, (err, value) => { 270 this.client.incr(this.prefix + key, (err, value) => {
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index 70ba7c303..59bce7520 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
64 } 64 }
65 65
66 const parentCommentThread = idx[childComment.inReplyToCommentId] 66 const parentCommentThread = idx[childComment.inReplyToCommentId]
67 if (!parentCommentThread) { 67 // Maybe the parent comment was blocked by the admin/user
68 const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}` 68 if (!parentCommentThread) continue
69 throw new Error(msg)
70 }
71 69
72 parentCommentThread.children.push(childCommentThread) 70 parentCommentThread.children.push(childCommentThread)
73 idx[childComment.id] = childCommentThread 71 idx[childComment.id] = childCommentThread
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index bf3ff78c2..a78de61e5 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,5 +1,5 @@
1import { CONFIG } from '../initializers' 1import { CONFIG } from '../initializers'
2import { join, extname } from 'path' 2import { extname, join } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, rename, stat } from 'fs-extra' 4import { copy, remove, rename, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
@@ -7,10 +7,11 @@ import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { VideoFileModel } from '../models/video/video-file'
8import { VideoModel } from '../models/video/video' 8import { VideoModel } from '../models/video/video'
9 9
10async function optimizeOriginalVideofile (video: VideoModel) { 10async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) {
11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
12 const newExtname = '.mp4' 12 const newExtname = '.mp4'
13 const inputVideoFile = video.getOriginalFile() 13
14 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
14 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) 15 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
15 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 16 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
16 17
@@ -124,7 +125,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) {
124} 125}
125 126
126export { 127export {
127 optimizeOriginalVideofile, 128 optimizeVideofile,
128 transcodeOriginalVideofile, 129 transcodeOriginalVideofile,
129 importVideoFile 130 importVideoFile
130} 131}
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1b44957d3..1e00fc731 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -8,7 +8,7 @@ const lock = new AsyncLock({ timeout: 5000 })
8 8
9function cacheRoute (lifetimeArg: string | number) { 9function cacheRoute (lifetimeArg: string | number) {
10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) { 10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
11 const redisKey = Redis.Instance.buildCachedRouteKey(req) 11 const redisKey = Redis.Instance.generateCachedRouteKey(req)
12 12
13 try { 13 try {
14 await lock.acquire(redisKey, async (done) => { 14 await lock.acquire(redisKey, async (done) => {
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
new file mode 100644
index 000000000..109276c63
--- /dev/null
+++ b/server/middlewares/validators/blocklist.ts
@@ -0,0 +1,172 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
6import { UserModel } from '../../models/account/user'
7import { AccountBlocklistModel } from '../../models/account/account-blocklist'
8import { isHostValid } from '../../helpers/custom-validators/servers'
9import { ServerBlocklistModel } from '../../models/server/server-blocklist'
10import { ServerModel } from '../../models/server/server'
11import { CONFIG } from '../../initializers'
12import { getServerActor } from '../../helpers/utils'
13
14const 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
38const 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
55const 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
72const 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
101const 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
116const 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
133export {
134 blockServerValidator,
135 blockAccountValidator,
136 unblockAccountByAccountValidator,
137 unblockServerByAccountValidator,
138 unblockAccountByServerValidator,
139 unblockServerByServerValidator
140}
141
142// ---------------------------------------------------------------------------
143
144async 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
159async function isUnblockServerExists (accountId: number, host: string, res: express.Response) {
160 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
161 if (!serverBlock) {
162 res.status(404)
163 .send({ error: 'Server block entry not found.' })
164 .end()
165
166 return false
167 }
168
169 res.locals.serverBlock = serverBlock
170
171 return true
172}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 940547a3e..46c7f0f3a 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './blocklist'
2export * from './oembed' 3export * from './oembed'
3export * from './activitypub' 4export * from './activitypub'
4export * from './pagination' 5export * from './pagination'
@@ -8,9 +9,6 @@ export * from './sort'
8export * from './users' 9export * from './users'
9export * from './user-subscriptions' 10export * from './user-subscriptions'
10export * from './videos' 11export * from './videos'
11export * from './video-abuses'
12export * from './video-blacklist'
13export * from './video-channels'
14export * from './webfinger' 12export * from './webfinger'
15export * from './search' 13export * from './search'
16export * from './video-imports' 14export * from './server'
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 8baf643a5..6a95d6095 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -2,8 +2,7 @@ import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from './utils'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { query } from 'express-validator/check' 4import { query } from 'express-validator/check'
5import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' 5import { isDateValid } from '../../helpers/custom-validators/misc'
6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
7 6
8const videosSearchValidator = [ 7const videosSearchValidator = [
9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 8 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [
35 } 34 }
36] 35]
37 36
38const commonVideosFiltersValidator = [
39 query('categoryOneOf')
40 .optional()
41 .customSanitizer(toArray)
42 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
43 query('licenceOneOf')
44 .optional()
45 .customSanitizer(toArray)
46 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
47 query('languageOneOf')
48 .optional()
49 .customSanitizer(toArray)
50 .custom(isStringArray).withMessage('Should have a valid one of language array'),
51 query('tagsOneOf')
52 .optional()
53 .customSanitizer(toArray)
54 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
55 query('tagsAllOf')
56 .optional()
57 .customSanitizer(toArray)
58 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
59 query('nsfw')
60 .optional()
61 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
62
63 (req: express.Request, res: express.Response, next: express.NextFunction) => {
64 logger.debug('Checking commons video filters query', { parameters: req.query })
65
66 if (areValidationErrors(req, res)) return
67
68 return next()
69 }
70]
71
72// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
73 38
74export { 39export {
75 commonVideosFiltersValidator,
76 videoChannelsSearchValidator, 40 videoChannelsSearchValidator,
77 videosSearchValidator 41 videosSearchValidator
78} 42}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
new file mode 100644
index 000000000..a491dfeb3
--- /dev/null
+++ b/server/middlewares/validators/server.ts
@@ -0,0 +1,33 @@
1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isHostValid } from '../../helpers/custom-validators/servers'
5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator/check'
7
8const 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
31export {
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
16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) 17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) 18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
19 21
20const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 22const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
21const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 23const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -31,6 +33,8 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
31const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 33const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
32const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) 34const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
33const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) 35const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
36const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
37const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
34 38
35// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
36 40
@@ -48,5 +52,7 @@ export {
48 jobsSortValidator, 52 jobsSortValidator,
49 videoCommentThreadsSortValidator, 53 videoCommentThreadsSortValidator,
50 userSubscriptionsSortValidator, 54 userSubscriptionsSortValidator,
51 videoChannelsSearchSortValidator 55 videoChannelsSearchSortValidator,
56 accountsBlocklistSortValidator,
57 serversBlocklistSortValidator
52} 58}
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
new file mode 100644
index 000000000..294783d85
--- /dev/null
+++ b/server/middlewares/validators/videos/index.ts
@@ -0,0 +1,8 @@
1export * from './video-abuses'
2export * from './video-blacklist'
3export * from './video-captions'
4export * from './video-channels'
5export * from './video-comments'
6export * from './video-imports'
7export * from './video-watch'
8export * from './videos'
diff --git a/server/middlewares/validators/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index f15d55a75..be26ca16a 100644
--- a/server/middlewares/validators/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,16 +1,16 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../helpers/custom-validators/videos' 5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from './utils' 7import { areValidationErrors } from '../utils'
8import { 8import {
9 isVideoAbuseExist, 9 isVideoAbuseExist,
10 isVideoAbuseModerationCommentValid, 10 isVideoAbuseModerationCommentValid,
11 isVideoAbuseReasonValid, 11 isVideoAbuseReasonValid,
12 isVideoAbuseStateValid 12 isVideoAbuseStateValid
13} from '../../helpers/custom-validators/video-abuses' 13} from '../../../helpers/custom-validators/video-abuses'
14 14
15const videoAbuseReportValidator = [ 15const videoAbuseReportValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 95a2b9f17..13da7acff 100644
--- a/server/middlewares/validators/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,10 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from './utils' 6import { areValidationErrors } from '../utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
8 8
9const videosBlacklistRemoveValidator = [ 9const videosBlacklistRemoveValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 51ffd7f3c..63d84fbec 100644
--- a/server/middlewares/validators/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from '../utils'
3import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos' 3import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
5import { body, param } from 'express-validator/check' 5import { body, param } from 'express-validator/check'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../../initializers'
7import { UserRight } from '../../../shared' 7import { UserRight } from '../../../../shared'
8import { logger } from '../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
10import { cleanUpReqFiles } from '../../helpers/express-utils' 10import { cleanUpReqFiles } from '../../../helpers/express-utils'
11 11
12const addVideoCaptionValidator = [ 12const addVideoCaptionValidator = [
13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 56a347b39..f039794e0 100644
--- a/server/middlewares/validators/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,20 +1,20 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 4import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts'
5import { 5import {
6 isLocalVideoChannelNameExist, 6 isLocalVideoChannelNameExist,
7 isVideoChannelDescriptionValid, 7 isVideoChannelDescriptionValid,
8 isVideoChannelNameValid, 8 isVideoChannelNameValid,
9 isVideoChannelNameWithHostExist, 9 isVideoChannelNameWithHostExist,
10 isVideoChannelSupportValid 10 isVideoChannelSupportValid
11} from '../../helpers/custom-validators/video-channels' 11} from '../../../helpers/custom-validators/video-channels'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { UserModel } from '../../models/account/user' 13import { UserModel } from '../../../models/account/user'
14import { VideoChannelModel } from '../../models/video/video-channel' 14import { VideoChannelModel } from '../../../models/video/video-channel'
15import { areValidationErrors } from './utils' 15import { areValidationErrors } from '../utils'
16import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' 16import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
17import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../../models/activitypub/actor'
18 18
19const listVideoAccountChannelsValidator = [ 19const listVideoAccountChannelsValidator = [
20 param('accountName').exists().withMessage('Should have a valid account name'), 20 param('accountName').exists().withMessage('Should have a valid account name'),
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 693852499..348d33082 100644
--- a/server/middlewares/validators/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' 5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { isVideoExist } from '../../helpers/custom-validators/videos' 6import { isVideoExist } from '../../../helpers/custom-validators/videos'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { UserModel } from '../../models/account/user' 8import { UserModel } from '../../../models/account/user'
9import { VideoModel } from '../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
10import { VideoCommentModel } from '../../models/video/video-comment' 10import { VideoCommentModel } from '../../../models/video/video-comment'
11import { areValidationErrors } from './utils' 11import { areValidationErrors } from '../utils'
12 12
13const listVideoCommentThreadsValidator = [ 13const listVideoCommentThreadsValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index b2063b8da..48d20f904 100644
--- a/server/middlewares/validators/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isIdValid } from '../../helpers/custom-validators/misc' 3import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from '../utils'
6import { getCommonVideoAttributes } from './videos' 6import { getCommonVideoAttributes } from './videos'
7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports' 7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
8import { cleanUpReqFiles } from '../../helpers/express-utils' 8import { cleanUpReqFiles } from '../../../helpers/express-utils'
9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos' 9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
10import { CONFIG } from '../../initializers/constants' 10import { CONFIG } from '../../../initializers/constants'
11import { CONSTRAINTS_FIELDS } from '../../initializers' 11import { CONSTRAINTS_FIELDS } from '../../../initializers'
12 12
13const videoImportAddValidator = getCommonVideoAttributes().concat([ 13const videoImportAddValidator = getCommonVideoAttributes().concat([
14 body('channelId') 14 body('channelId')
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
new file mode 100644
index 000000000..bca64662f
--- /dev/null
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -0,0 +1,28 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger'
7
8const videoWatchingValidator = [
9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
10 body('currentTime')
11 .toInt()
12 .isInt().withMessage('Should have correct current time'),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 logger.debug('Checking videoWatching parameters', { parameters: req.body })
16
17 if (areValidationErrors(req, res)) return
18 if (!await isVideoExist(req.params.videoId, res, 'id')) return
19
20 return next()
21 }
22]
23
24// ---------------------------------------------------------------------------
25
26export {
27 videoWatchingValidator
28}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos/videos.ts
index 67eabe468..9dc52a134 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -1,16 +1,17 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param, ValidationChain } from 'express-validator/check' 3import { body, param, query, ValidationChain } from 'express-validator/check'
4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' 4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5import { 5import {
6 isBooleanValid, 6 isBooleanValid,
7 isDateValid, 7 isDateValid,
8 isIdOrUUIDValid, 8 isIdOrUUIDValid,
9 isIdValid, 9 isIdValid,
10 isUUIDValid, 10 isUUIDValid,
11 toArray,
11 toIntOrNull, 12 toIntOrNull,
12 toValueOrNull 13 toValueOrNull
13} from '../../helpers/custom-validators/misc' 14} from '../../../helpers/custom-validators/misc'
14import { 15import {
15 checkUserCanManageVideo, 16 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid, 17 isScheduleVideoUpdatePrivacyValid,
@@ -19,6 +20,7 @@ import {
19 isVideoDescriptionValid, 20 isVideoDescriptionValid,
20 isVideoExist, 21 isVideoExist,
21 isVideoFile, 22 isVideoFile,
23 isVideoFilterValid,
22 isVideoImage, 24 isVideoImage,
23 isVideoLanguageValid, 25 isVideoLanguageValid,
24 isVideoLicenceValid, 26 isVideoLicenceValid,
@@ -27,21 +29,22 @@ import {
27 isVideoRatingTypeValid, 29 isVideoRatingTypeValid,
28 isVideoSupportValid, 30 isVideoSupportValid,
29 isVideoTagsValid 31 isVideoTagsValid
30} from '../../helpers/custom-validators/videos' 32} from '../../../helpers/custom-validators/videos'
31import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' 33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
32import { logger } from '../../helpers/logger' 34import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../initializers' 35import { CONSTRAINTS_FIELDS } from '../../../initializers'
34import { VideoShareModel } from '../../models/video/video-share' 36import { VideoShareModel } from '../../../models/video/video-share'
35import { authenticate } from '../oauth' 37import { authenticate } from '../../oauth'
36import { areValidationErrors } from './utils' 38import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../helpers/express-utils' 39import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../models/video/video' 40import { VideoModel } from '../../../models/video/video'
39import { UserModel } from '../../models/account/user' 41import { UserModel } from '../../../models/account/user'
40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' 42import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' 43import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' 44import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43import { AccountModel } from '../../models/account/account' 45import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../helpers/video' 46import { VideoFetchType } from '../../../helpers/video'
47import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45 48
46const videosAddValidator = getCommonVideoAttributes().concat([ 49const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile') 50 body('videofile')
@@ -69,7 +72,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
69 if (isAble === false) { 72 if (isAble === false) {
70 res.status(403) 73 res.status(403)
71 .json({ error: 'The user video quota is exceeded with this video.' }) 74 .json({ error: 'The user video quota is exceeded with this video.' })
72 .end()
73 75
74 return cleanUpReqFiles(req) 76 return cleanUpReqFiles(req)
75 } 77 }
@@ -82,7 +84,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
82 logger.error('Invalid input file in videosAddValidator.', { err }) 84 logger.error('Invalid input file in videosAddValidator.', { err })
83 res.status(400) 85 res.status(400)
84 .json({ error: 'Invalid input file.' }) 86 .json({ error: 'Invalid input file.' })
85 .end()
86 87
87 return cleanUpReqFiles(req) 88 return cleanUpReqFiles(req)
88 } 89 }
@@ -120,7 +121,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
120 cleanUpReqFiles(req) 121 cleanUpReqFiles(req)
121 return res.status(409) 122 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' }) 123 .json({ error: 'Cannot set "private" a video that was not private.' })
123 .end()
124 } 124 }
125 125
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -150,7 +150,6 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { 150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
151 return res.status(403) 151 return res.status(403)
152 .json({ error: 'Cannot get this private or blacklisted video.' }) 152 .json({ error: 'Cannot get this private or blacklisted video.' })
153 .end()
154 } 153 }
155 154
156 return next() 155 return next()
@@ -239,8 +238,8 @@ const videosChangeOwnershipValidator = [
239 const nextOwner = await AccountModel.loadLocalByName(req.body.username) 238 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
240 if (!nextOwner) { 239 if (!nextOwner) {
241 res.status(400) 240 res.status(400)
242 .type('json') 241 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
243 .end() 242
244 return 243 return
245 } 244 }
246 res.locals.nextOwner = nextOwner 245 res.locals.nextOwner = nextOwner
@@ -271,7 +270,7 @@ const videosTerminateChangeOwnershipValidator = [
271 } else { 270 } else {
272 res.status(403) 271 res.status(403)
273 .json({ error: 'Ownership already accepted or refused' }) 272 .json({ error: 'Ownership already accepted or refused' })
274 .end() 273
275 return 274 return
276 } 275 }
277 } 276 }
@@ -288,7 +287,7 @@ const videosAcceptChangeOwnershipValidator = [
288 if (isAble === false) { 287 if (isAble === false) {
289 res.status(403) 288 res.status(403)
290 .json({ error: 'The user video quota is exceeded with this video.' }) 289 .json({ error: 'The user video quota is exceeded with this video.' })
291 .end() 290
292 return 291 return
293 } 292 }
294 293
@@ -363,6 +362,51 @@ function getCommonVideoAttributes () {
363 ] as (ValidationChain | express.Handler)[] 362 ] as (ValidationChain | express.Handler)[]
364} 363}
365 364
365const commonVideosFiltersValidator = [
366 query('categoryOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
370 query('licenceOneOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
374 query('languageOneOf')
375 .optional()
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of language array'),
378 query('tagsOneOf')
379 .optional()
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
382 query('tagsAllOf')
383 .optional()
384 .customSanitizer(toArray)
385 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
386 query('nsfw')
387 .optional()
388 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
389 query('filter')
390 .optional()
391 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
392
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 logger.debug('Checking commons video filters query', { parameters: req.query })
395
396 if (areValidationErrors(req, res)) return
397
398 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
400 res.status(401)
401 .json({ error: 'You are not allowed to see all local videos.' })
402
403 return
404 }
405
406 return next()
407 }
408]
409
366// --------------------------------------------------------------------------- 410// ---------------------------------------------------------------------------
367 411
368export { 412export {
@@ -379,7 +423,9 @@ export {
379 videosTerminateChangeOwnershipValidator, 423 videosTerminateChangeOwnershipValidator,
380 videosAcceptChangeOwnershipValidator, 424 videosAcceptChangeOwnershipValidator,
381 425
382 getCommonVideoAttributes 426 getCommonVideoAttributes,
427
428 commonVideosFiltersValidator
383} 429}
384 430
385// --------------------------------------------------------------------------- 431// ---------------------------------------------------------------------------
@@ -389,7 +435,6 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
389 if (!req.body.scheduleUpdate.updateAt) { 435 if (!req.body.scheduleUpdate.updateAt) {
390 res.status(400) 436 res.status(400)
391 .json({ error: 'Schedule update at is mandatory.' }) 437 .json({ error: 'Schedule update at is mandatory.' })
392 .end()
393 438
394 return true 439 return true
395 } 440 }
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
new file mode 100644
index 000000000..fa2819235
--- /dev/null
+++ b/server/models/account/account-blocklist.ts
@@ -0,0 +1,111 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5
6enum 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})
39export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
40
41 @CreatedAt
42 createdAt: Date
43
44 @UpdatedAt
45 updatedAt: Date
46
47 @ForeignKey(() => AccountModel)
48 @Column
49 accountId: number
50
51 @BelongsTo(() => AccountModel, {
52 foreignKey: {
53 name: 'accountId',
54 allowNull: false
55 },
56 as: 'ByAccount',
57 onDelete: 'CASCADE'
58 })
59 ByAccount: AccountModel
60
61 @ForeignKey(() => AccountModel)
62 @Column
63 targetAccountId: number
64
65 @BelongsTo(() => AccountModel, {
66 foreignKey: {
67 name: 'targetAccountId',
68 allowNull: false
69 },
70 as: 'BlockedAccount',
71 onDelete: 'CASCADE'
72 })
73 BlockedAccount: AccountModel
74
75 static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
76 const query = {
77 where: {
78 accountId,
79 targetAccountId
80 }
81 }
82
83 return AccountBlocklistModel.findOne(query)
84 }
85
86 static listForApi (accountId: number, start: number, count: number, sort: string) {
87 const query = {
88 offset: start,
89 limit: count,
90 order: getSort(sort),
91 where: {
92 accountId
93 }
94 }
95
96 return AccountBlocklistModel
97 .scope([ ScopeNames.WITH_ACCOUNTS ])
98 .findAndCountAll(query)
99 .then(({ rows, count }) => {
100 return { total: count, data: rows }
101 })
102 }
103
104 toFormattedJSON (): AccountBlock {
105 return {
106 byAccount: this.ByAccount.toFormattedJSON(),
107 blockedAccount: this.BlockedAccount.toFormattedJSON(),
108 createdAt: this.createdAt
109 }
110 }
111}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 27c75d886..5a237d733 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> {
248 displayName: this.getDisplayName(), 248 displayName: this.getDisplayName(),
249 description: this.description, 249 description: this.description,
250 createdAt: this.createdAt, 250 createdAt: this.createdAt,
251 updatedAt: this.updatedAt 251 updatedAt: this.updatedAt,
252 userId: this.userId ? this.userId : undefined
252 } 253 }
253 254
254 return Object.assign(actor, account) 255 return Object.assign(actor, account)
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
new file mode 100644
index 000000000..0476cad9d
--- /dev/null
+++ b/server/models/account/user-video-history.ts
@@ -0,0 +1,55 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4
5@Table({
6 tableName: 'userVideoHistory',
7 indexes: [
8 {
9 fields: [ 'userId', 'videoId' ],
10 unique: true
11 },
12 {
13 fields: [ 'userId' ]
14 },
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @IsInt
29 @Column
30 currentTime: number
31
32 @ForeignKey(() => VideoModel)
33 @Column
34 videoId: number
35
36 @BelongsTo(() => VideoModel, {
37 foreignKey: {
38 allowNull: false
39 },
40 onDelete: 'CASCADE'
41 })
42 Video: VideoModel
43
44 @ForeignKey(() => UserModel)
45 @Column
46 userId: number
47
48 @BelongsTo(() => UserModel, {
49 foreignKey: {
50 allowNull: false
51 },
52 onDelete: 'CASCADE'
53 })
54 User: UserModel
55}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e56b0bf40..34aafa1a7 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -31,7 +31,8 @@ import {
31 isUserRoleValid, 31 isUserRoleValid,
32 isUserUsernameValid, 32 isUserUsernameValid,
33 isUserVideoQuotaDailyValid, 33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid 34 isUserVideoQuotaValid,
35 isUserWebTorrentEnabledValid
35} from '../../helpers/custom-validators/users' 36} from '../../helpers/custom-validators/users'
36import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 37import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
37import { OAuthTokenModel } from '../oauth/oauth-token' 38import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -109,6 +110,12 @@ export class UserModel extends Model<UserModel> {
109 110
110 @AllowNull(false) 111 @AllowNull(false)
111 @Default(true) 112 @Default(true)
113 @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
114 @Column
115 webTorrentEnabled: boolean
116
117 @AllowNull(false)
118 @Default(true)
112 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) 119 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
113 @Column 120 @Column
114 autoPlayVideo: boolean 121 autoPlayVideo: boolean
@@ -181,7 +188,25 @@ export class UserModel extends Model<UserModel> {
181 return this.count() 188 return this.count()
182 } 189 }
183 190
184 static listForApi (start: number, count: number, sort: string) { 191 static listForApi (start: number, count: number, sort: string, search?: string) {
192 let where = undefined
193 if (search) {
194 where = {
195 [Sequelize.Op.or]: [
196 {
197 email: {
198 [Sequelize.Op.iLike]: '%' + search + '%'
199 }
200 },
201 {
202 username: {
203 [ Sequelize.Op.iLike ]: '%' + search + '%'
204 }
205 }
206 ]
207 }
208 }
209
185 const query = { 210 const query = {
186 attributes: { 211 attributes: {
187 include: [ 212 include: [
@@ -204,7 +229,8 @@ export class UserModel extends Model<UserModel> {
204 }, 229 },
205 offset: start, 230 offset: start,
206 limit: count, 231 limit: count,
207 order: getSort(sort) 232 order: getSort(sort),
233 where
208 } 234 }
209 235
210 return UserModel.findAndCountAll(query) 236 return UserModel.findAndCountAll(query)
@@ -336,6 +362,7 @@ export class UserModel extends Model<UserModel> {
336 email: this.email, 362 email: this.email,
337 emailVerified: this.emailVerified, 363 emailVerified: this.emailVerified,
338 nsfwPolicy: this.nsfwPolicy, 364 nsfwPolicy: this.nsfwPolicy,
365 webTorrentEnabled: this.webTorrentEnabled,
339 autoPlayVideo: this.autoPlayVideo, 366 autoPlayVideo: this.autoPlayVideo,
340 role: this.role, 367 role: this.role,
341 roleLabel: USER_ROLE_LABELS[ this.role ], 368 roleLabel: USER_ROLE_LABELS[ this.role ],
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 27bb43dae..3373355ef 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -280,7 +280,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
280 return ActorFollowModel.findAll(query) 280 return ActorFollowModel.findAll(query)
281 } 281 }
282 282
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 283 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
284 const query = { 284 const query = {
285 distinct: true, 285 distinct: true,
286 offset: start, 286 offset: start,
@@ -299,7 +299,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
299 model: ActorModel, 299 model: ActorModel,
300 as: 'ActorFollowing', 300 as: 'ActorFollowing',
301 required: true, 301 required: true,
302 include: [ ServerModel ] 302 include: [
303 {
304 model: ServerModel,
305 required: true,
306 where: search ? {
307 host: {
308 [Sequelize.Op.iLike]: '%' + search + '%'
309 }
310 } : undefined
311 }
312 ]
303 } 313 }
304 ] 314 ]
305 } 315 }
@@ -313,6 +323,49 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
313 }) 323 })
314 } 324 }
315 325
326 static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) {
327 const query = {
328 distinct: true,
329 offset: start,
330 limit: count,
331 order: getSort(sort),
332 include: [
333 {
334 model: ActorModel,
335 required: true,
336 as: 'ActorFollower',
337 include: [
338 {
339 model: ServerModel,
340 required: true,
341 where: search ? {
342 host: {
343 [ Sequelize.Op.iLike ]: '%' + search + '%'
344 }
345 } : undefined
346 }
347 ]
348 },
349 {
350 model: ActorModel,
351 as: 'ActorFollowing',
352 required: true,
353 where: {
354 id
355 }
356 }
357 ]
358 }
359
360 return ActorFollowModel.findAndCountAll(query)
361 .then(({ rows, count }) => {
362 return {
363 data: rows,
364 total: count
365 }
366 })
367 }
368
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 369 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
317 const query = { 370 const query = {
318 attributes: [], 371 attributes: [],
@@ -370,39 +423,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
370 }) 423 })
371 } 424 }
372 425
373 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
374 const query = {
375 distinct: true,
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 include: [
380 {
381 model: ActorModel,
382 required: true,
383 as: 'ActorFollower',
384 include: [ ServerModel ]
385 },
386 {
387 model: ActorModel,
388 as: 'ActorFollowing',
389 required: true,
390 where: {
391 id
392 }
393 }
394 ]
395 }
396
397 return ActorFollowModel.findAndCountAll(query)
398 .then(({ rows, count }) => {
399 return {
400 data: rows,
401 total: count
402 }
403 })
404 }
405
406 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 426 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
407 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 427 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
408 } 428 }
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 2ebe23ef1..cbfc7f7fa 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -408,6 +408,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
408 url: { 408 url: {
409 type: 'Link', 409 type: 'Link',
410 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 410 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
411 mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
411 href: this.fileUrl, 412 href: this.fileUrl,
412 height: this.VideoFile.resolution, 413 height: this.VideoFile.resolution,
413 size: this.VideoFile.size, 414 size: this.VideoFile.size,
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
new file mode 100644
index 000000000..450f27152
--- /dev/null
+++ b/server/models/server/server-blocklist.ts
@@ -0,0 +1,121 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort } from '../utils'
6
7enum 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})
43export 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
67function 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
69export { 84export {
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { 2import {
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'
6import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' 17import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
@@ -13,9 +24,11 @@ import { AccountModel } from '../account/account'
13import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
14import { AvatarModel } from '../avatar/avatar' 25import { AvatarModel } from '../avatar/avatar'
15import { ServerModel } from '../server/server' 26import { ServerModel } from '../server/server'
16import { getSort, throwIfNotValid } from '../utils' 27import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
17import { VideoModel } from './video' 28import { VideoModel } from './video'
18import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user'
19 32
20enum ScopeNames { 33enum ScopeNames {
21 WITH_ACCOUNT = 'WITH_ACCOUNT', 34 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -25,18 +38,29 @@ enum ScopeNames {
25} 38}
26 39
27@Scopes({ 40@Scopes({
28 [ScopeNames.ATTRIBUTES_FOR_API]: { 41 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
29 attributes: { 42 return {
30 include: [ 43 attributes: {
31 [ 44 include: [
32 Sequelize.literal( 45 [
33 '(SELECT COUNT("replies"."id") ' + 46 Sequelize.literal(
34 'FROM "videoComment" AS "replies" ' + 47 '(' +
35 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' 48 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
36 ), 49 'SELECT COUNT("replies"."id") - (' +
37 'totalReplies' 50 'SELECT COUNT("replies"."id") ' +
51 'FROM "videoComment" AS "replies" ' +
52 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
53 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
54 ')' +
55 'FROM "videoComment" AS "replies" ' +
56 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
57 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
58 ')'
59 ),
60 'totalReplies'
61 ]
38 ] 62 ]
39 ] 63 }
40 } 64 }
41 }, 65 },
42 [ScopeNames.WITH_ACCOUNT]: { 66 [ScopeNames.WITH_ACCOUNT]: {
@@ -267,26 +291,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
267 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) 291 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
268 } 292 }
269 293
270 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { 294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
295 const serverActor = await getServerActor()
296 const serverAccountId = serverActor.Account.id
297 const userAccountId = user ? user.Account.id : undefined
298
271 const query = { 299 const query = {
272 offset: start, 300 offset: start,
273 limit: count, 301 limit: count,
274 order: getSort(sort), 302 order: getSort(sort),
275 where: { 303 where: {
276 videoId, 304 videoId,
277 inReplyToCommentId: null 305 inReplyToCommentId: null,
306 accountId: {
307 [Sequelize.Op.notIn]: Sequelize.literal(
308 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
309 )
310 }
278 } 311 }
279 } 312 }
280 313
314 // FIXME: typings
315 const scopes: any[] = [
316 ScopeNames.WITH_ACCOUNT,
317 {
318 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
319 }
320 ]
321
281 return VideoCommentModel 322 return VideoCommentModel
282 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 323 .scope(scopes)
283 .findAndCountAll(query) 324 .findAndCountAll(query)
284 .then(({ rows, count }) => { 325 .then(({ rows, count }) => {
285 return { total: count, data: rows } 326 return { total: count, data: rows }
286 }) 327 })
287 } 328 }
288 329
289 static listThreadCommentsForApi (videoId: number, threadId: number) { 330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
331 const serverActor = await getServerActor()
332 const serverAccountId = serverActor.Account.id
333 const userAccountId = user ? user.Account.id : undefined
334
290 const query = { 335 const query = {
291 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
292 where: { 337 where: {
@@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
294 [ Sequelize.Op.or ]: [ 339 [ Sequelize.Op.or ]: [
295 { id: threadId }, 340 { id: threadId },
296 { originCommentId: threadId } 341 { originCommentId: threadId }
297 ] 342 ],
343 accountId: {
344 [Sequelize.Op.notIn]: Sequelize.literal(
345 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
346 )
347 }
298 } 348 }
299 } 349 }
300 350
351 const scopes: any[] = [
352 ScopeNames.WITH_ACCOUNT,
353 {
354 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
355 }
356 ]
357
301 return VideoCommentModel 358 return VideoCommentModel
302 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 359 .scope(scopes)
303 .findAndCountAll(query) 360 .findAndCountAll(query)
304 .then(({ rows, count }) => { 361 .then(({ rows, count }) => {
305 return { total: count, data: rows } 362 return { total: count, data: rows }
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..e3f8d525b 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,6 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc'
13 14
14export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
15 completeDescription?: boolean 16 completeDescription?: boolean
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
24 const formattedAccount = video.VideoChannel.Account.toFormattedJSON() 25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
25 const formattedVideoChannel = video.VideoChannel.toFormattedJSON() 26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
26 27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29
27 const videoObject: Video = { 30 const videoObject: Video = {
28 id: video.id, 31 id: video.id,
29 uuid: video.uuid, 32 uuid: video.uuid,
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
74 url: formattedVideoChannel.url, 77 url: formattedVideoChannel.url,
75 host: formattedVideoChannel.host, 78 host: formattedVideoChannel.host,
76 avatar: formattedVideoChannel.avatar 79 avatar: formattedVideoChannel.avatar
77 } 80 },
81
82 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime
84 } : undefined
78 } 85 }
79 86
80 if (options) { 87 if (options) {
@@ -201,6 +208,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
201 url.push({ 208 url.push({
202 type: 'Link', 209 type: 'Link',
203 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 210 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
211 mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
204 href: video.getVideoFileUrl(file, baseUrlHttp), 212 href: video.getVideoFileUrl(file, baseUrlHttp),
205 height: file.resolution, 213 height: file.resolution,
206 size: file.size, 214 size: file.size,
@@ -210,6 +218,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
210 url.push({ 218 url.push({
211 type: 'Link', 219 type: 'Link',
212 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', 220 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
221 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
213 href: video.getTorrentUrl(file, baseUrlHttp), 222 href: video.getTorrentUrl(file, baseUrlHttp),
214 height: file.resolution 223 height: file.resolution
215 }) 224 })
@@ -217,6 +226,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
217 url.push({ 226 url.push({
218 type: 'Link', 227 type: 'Link',
219 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', 228 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
229 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
220 href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), 230 href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
221 height: file.resolution 231 height: file.resolution
222 }) 232 })
@@ -226,6 +236,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
226 url.push({ 236 url.push({
227 type: 'Link', 237 type: 'Link',
228 mimeType: 'text/html', 238 mimeType: 'text/html',
239 mediaType: 'text/html',
229 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 240 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
230 }) 241 })
231 242
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c89c16bf..6c183933b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -27,7 +27,7 @@ import {
27 Table, 27 Table,
28 UpdatedAt 28 UpdatedAt
29} from 'sequelize-typescript' 29} from 'sequelize-typescript'
30import { VideoPrivacy, VideoState } from '../../../shared' 30import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
33import { VideoFilter } from '../../../shared/models/videos/video-query.type' 33import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
70import { ActorModel } from '../activitypub/actor' 70import { ActorModel } from '../activitypub/actor'
71import { AvatarModel } from '../avatar/avatar' 71import { AvatarModel } from '../avatar/avatar'
72import { ServerModel } from '../server/server' 72import { ServerModel } from '../server/server'
73import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' 73import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
74import { TagModel } from './tag' 74import { TagModel } from './tag'
75import { VideoAbuseModel } from './video-abuse' 75import { VideoAbuseModel } from './video-abuse'
76import { VideoChannelModel } from './video-channel' 76import { VideoChannelModel } from './video-channel'
@@ -92,6 +92,8 @@ import {
92 videoModelToFormattedJSON 92 videoModelToFormattedJSON
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user'
95 97
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +129,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 129 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 130 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 131 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 132 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
133 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 134}
132 135
133type ForAPIOptions = { 136type ForAPIOptions = {
@@ -136,6 +139,7 @@ type ForAPIOptions = {
136} 139}
137 140
138type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number
139 actorId: number 143 actorId: number
140 includeLocalVideos: boolean 144 includeLocalVideos: boolean
141 filter?: VideoFilter 145 filter?: VideoFilter
@@ -149,6 +153,7 @@ type AvailableForListIDsOptions = {
149 accountId?: number 153 accountId?: number
150 videoChannelId?: number 154 videoChannelId?: number
151 trendingDays?: number 155 trendingDays?: number
156 user?: UserModel
152} 157}
153 158
154@Scopes({ 159@Scopes({
@@ -234,6 +239,22 @@ type AvailableForListIDsOptions = {
234 } 239 }
235 ] 240 ]
236 }, 241 },
242 channelId: {
243 [ Sequelize.Op.notIn ]: Sequelize.literal(
244 '(' +
245 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
246 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
247 ')' +
248 ')'
249 )
250 }
251 },
252 include: []
253 }
254
255 // Only list public/published videos
256 if (!options.filter || options.filter !== 'all-local') {
257 const privacyWhere = {
237 // Always list public videos 258 // Always list public videos
238 privacy: VideoPrivacy.PUBLIC, 259 privacy: VideoPrivacy.PUBLIC,
239 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 260 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -248,8 +269,9 @@ type AvailableForListIDsOptions = {
248 } 269 }
249 } 270 }
250 ] 271 ]
251 }, 272 }
252 include: [] 273
274 Object.assign(query.where, privacyWhere)
253 } 275 }
254 276
255 if (options.filter || options.accountId || options.videoChannelId) { 277 if (options.filter || options.accountId || options.videoChannelId) {
@@ -464,6 +486,8 @@ type AvailableForListIDsOptions = {
464 include: [ 486 include: [
465 { 487 {
466 model: () => VideoFileModel.unscoped(), 488 model: () => VideoFileModel.unscoped(),
489 // FIXME: typings
490 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 491 required: false,
468 include: [ 492 include: [
469 { 493 {
@@ -482,6 +506,20 @@ type AvailableForListIDsOptions = {
482 required: false 506 required: false
483 } 507 }
484 ] 508 ]
509 },
510 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
511 return {
512 include: [
513 {
514 attributes: [ 'currentTime' ],
515 model: UserVideoHistoryModel.unscoped(),
516 required: false,
517 where: {
518 userId
519 }
520 }
521 ]
522 }
485 } 523 }
486}) 524})
487@Table({ 525@Table({
@@ -672,11 +710,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 710 name: 'videoId',
673 allowNull: false 711 allowNull: false
674 }, 712 },
675 onDelete: 'cascade', 713 onDelete: 'cascade'
676 hooks: true
677 }) 714 })
678 VideoViews: VideoViewModel[] 715 VideoViews: VideoViewModel[]
679 716
717 @HasMany(() => UserVideoHistoryModel, {
718 foreignKey: {
719 name: 'videoId',
720 allowNull: false
721 },
722 onDelete: 'cascade'
723 })
724 UserVideoHistories: UserVideoHistoryModel[]
725
680 @HasOne(() => ScheduleVideoUpdateModel, { 726 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 727 foreignKey: {
682 name: 'videoId', 728 name: 'videoId',
@@ -762,6 +808,16 @@ export class VideoModel extends Model<VideoModel> {
762 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 808 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
763 } 809 }
764 810
811 static listLocal () {
812 const query = {
813 where: {
814 remote: false
815 }
816 }
817
818 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query)
819 }
820
765 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 821 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
766 function getRawQuery (select: string) { 822 function getRawQuery (select: string) {
767 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + 823 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
@@ -930,8 +986,13 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 986 accountId?: number,
931 videoChannelId?: number, 987 videoChannelId?: number,
932 actorId?: number 988 actorId?: number
933 trendingDays?: number 989 trendingDays?: number,
990 user?: UserModel
934 }, countVideos = true) { 991 }, countVideos = true) {
992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
993 throw new Error('Try to filter all-local but no user has not the see all videos right')
994 }
995
935 const query: IFindOptions<VideoModel> = { 996 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 997 offset: options.start,
937 limit: options.count, 998 limit: options.count,
@@ -945,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
945 query.group = 'VideoModel.id' 1006 query.group = 'VideoModel.id'
946 } 1007 }
947 1008
1009 const serverActor = await getServerActor()
1010
948 // actorId === null has a meaning, so just check undefined 1011 // actorId === null has a meaning, so just check undefined
949 const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id 1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
950 1013
951 const queryOptions = { 1014 const queryOptions = {
952 actorId, 1015 actorId,
1016 serverAccountId: serverActor.Account.id,
953 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
954 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
955 licenceOneOf: options.licenceOneOf, 1019 licenceOneOf: options.licenceOneOf,
@@ -961,6 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 1025 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 1026 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 1027 includeLocalVideos: options.includeLocalVideos,
1028 user: options.user,
964 trendingDays 1029 trendingDays
965 } 1030 }
966 1031
@@ -983,6 +1048,8 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1048 tagsAllOf?: string[]
984 durationMin?: number // seconds 1049 durationMin?: number // seconds
985 durationMax?: number // seconds 1050 durationMax?: number // seconds
1051 user?: UserModel,
1052 filter?: VideoFilter
986 }) { 1053 }) {
987 const whereAnd = [] 1054 const whereAnd = []
988 1055
@@ -1052,13 +1119,16 @@ export class VideoModel extends Model<VideoModel> {
1052 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1053 const queryOptions = { 1120 const queryOptions = {
1054 actorId: serverActor.id, 1121 actorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id,
1055 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1056 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
1057 categoryOneOf: options.categoryOneOf, 1125 categoryOneOf: options.categoryOneOf,
1058 licenceOneOf: options.licenceOneOf, 1126 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1127 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1128 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1129 tagsAllOf: options.tagsAllOf,
1130 user: options.user,
1131 filter: options.filter
1062 } 1132 }
1063 1133
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1134 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1195,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1195 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1196 }
1127 1197
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1198 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1199 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1200
1131 const options = { 1201 const options = {
@@ -1134,14 +1204,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1204 transaction: t
1135 } 1205 }
1136 1206
1207 const scopes = [
1208 ScopeNames.WITH_TAGS,
1209 ScopeNames.WITH_BLACKLISTED,
1210 ScopeNames.WITH_FILES,
1211 ScopeNames.WITH_ACCOUNT_DETAILS,
1212 ScopeNames.WITH_SCHEDULED_UPDATE
1213 ]
1214
1215 if (userId) {
1216 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1217 }
1218
1137 return VideoModel 1219 return VideoModel
1138 .scope([ 1220 .scope(scopes)
1139 ScopeNames.WITH_TAGS,
1140 ScopeNames.WITH_BLACKLISTED,
1141 ScopeNames.WITH_FILES,
1142 ScopeNames.WITH_ACCOUNT_DETAILS,
1143 ScopeNames.WITH_SCHEDULED_UPDATE
1144 ])
1145 .findOne(options) 1221 .findOne(options)
1146 } 1222 }
1147 1223
@@ -1179,9 +1255,11 @@ export class VideoModel extends Model<VideoModel> {
1179 1255
1180 // threshold corresponds to how many video the field should have to be returned 1256 // threshold corresponds to how many video the field should have to be returned
1181 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1257 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1182 const actorId = (await getServerActor()).id 1258 const serverActor = await getServerActor()
1259 const actorId = serverActor.id
1183 1260
1184 const scopeOptions = { 1261 const scopeOptions: AvailableForListIDsOptions = {
1262 serverAccountId: serverActor.Account.id,
1185 actorId, 1263 actorId,
1186 includeLocalVideos: true 1264 includeLocalVideos: true
1187 } 1265 }
@@ -1216,7 +1294,7 @@ export class VideoModel extends Model<VideoModel> {
1216 } 1294 }
1217 1295
1218 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1296 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1219 if (filter && filter === 'local') { 1297 if (filter && (filter === 'local' || filter === 'all-local')) {
1220 return { 1298 return {
1221 serverId: null 1299 serverId: null
1222 } 1300 }
@@ -1225,7 +1303,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1303 return {}
1226 } 1304 }
1227 1305
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1306 private static async getAvailableForApi (
1307 query: IFindOptions<VideoModel>,
1308 options: AvailableForListIDsOptions,
1309 countVideos = true
1310 ) {
1229 const idsScope = { 1311 const idsScope = {
1230 method: [ 1312 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1313 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1331,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1331
1250 if (ids.length === 0) return { data: [], total: count } 1332 if (ids.length === 0) return { data: [], total: count }
1251 1333
1252 const apiScope = { 1334 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1335 const apiScope: any[] = [
1336 {
1337 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1338 }
1339 ]
1340
1341 if (options.user) {
1342 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1254 } 1343 }
1255 1344
1256 const secondQuery = { 1345 const secondQuery = {
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
new file mode 100644
index 000000000..c745ac975
--- /dev/null
+++ b/server/tests/api/check-params/blocklist.ts
@@ -0,0 +1,494 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeDeleteRequest,
12 makeGetRequest,
13 makePostBodyRequest,
14 ServerInfo,
15 setAccessTokensToServers, userLogin
16} from '../../utils'
17import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
18
19describe('Test blocklist API validators', function () {
20 let servers: ServerInfo[]
21 let server: ServerInfo
22 let userAccessToken: string
23
24 before(async function () {
25 this.timeout(60000)
26
27 await flushTests()
28
29 servers = await flushAndRunMultipleServers(2)
30 await setAccessTokensToServers(servers)
31
32 server = servers[0]
33
34 const user = { username: 'user1', password: 'password' }
35 await createUser(server.url, server.accessToken, user.username, user.password)
36
37 userAccessToken = await userLogin(server, user)
38
39 await doubleFollow(servers[0], servers[1])
40 })
41
42 // ---------------------------------------------------------------
43
44 describe('When managing user blocklist', function () {
45
46 describe('When managing user accounts blocklist', function () {
47 const path = '/api/v1/users/me/blocklist/accounts'
48
49 describe('When listing blocked accounts', function () {
50 it('Should fail with an unauthenticated user', async function () {
51 await makeGetRequest({
52 url: server.url,
53 path,
54 statusCodeExpected: 401
55 })
56 })
57
58 it('Should fail with a bad start pagination', async function () {
59 await checkBadStartPagination(server.url, path, server.accessToken)
60 })
61
62 it('Should fail with a bad count pagination', async function () {
63 await checkBadCountPagination(server.url, path, server.accessToken)
64 })
65
66 it('Should fail with an incorrect sort', async function () {
67 await checkBadSortPagination(server.url, path, server.accessToken)
68 })
69 })
70
71 describe('When blocking an account', function () {
72 it('Should fail with an unauthenticated user', async function () {
73 await makePostBodyRequest({
74 url: server.url,
75 path,
76 fields: { accountName: 'user1' },
77 statusCodeExpected: 401
78 })
79 })
80
81 it('Should fail with an unknown account', async function () {
82 await makePostBodyRequest({
83 url: server.url,
84 token: server.accessToken,
85 path,
86 fields: { accountName: 'user2' },
87 statusCodeExpected: 404
88 })
89 })
90
91 it('Should fail to block ourselves', async function () {
92 await makePostBodyRequest({
93 url: server.url,
94 token: server.accessToken,
95 path,
96 fields: { accountName: 'root' },
97 statusCodeExpected: 409
98 })
99 })
100
101 it('Should succeed with the correct params', async function () {
102 await makePostBodyRequest({
103 url: server.url,
104 token: server.accessToken,
105 path,
106 fields: { accountName: 'user1' },
107 statusCodeExpected: 204
108 })
109 })
110 })
111
112 describe('When unblocking an account', function () {
113 it('Should fail with an unauthenticated user', async function () {
114 await makeDeleteRequest({
115 url: server.url,
116 path: path + '/user1',
117 statusCodeExpected: 401
118 })
119 })
120
121 it('Should fail with an unknown account block', async function () {
122 await makeDeleteRequest({
123 url: server.url,
124 path: path + '/user2',
125 token: server.accessToken,
126 statusCodeExpected: 404
127 })
128 })
129
130 it('Should succeed with the correct params', async function () {
131 await makeDeleteRequest({
132 url: server.url,
133 path: path + '/user1',
134 token: server.accessToken,
135 statusCodeExpected: 204
136 })
137 })
138 })
139 })
140
141 describe('When managing user servers blocklist', function () {
142 const path = '/api/v1/users/me/blocklist/servers'
143
144 describe('When listing blocked servers', function () {
145 it('Should fail with an unauthenticated user', async function () {
146 await makeGetRequest({
147 url: server.url,
148 path,
149 statusCodeExpected: 401
150 })
151 })
152
153 it('Should fail with a bad start pagination', async function () {
154 await checkBadStartPagination(server.url, path, server.accessToken)
155 })
156
157 it('Should fail with a bad count pagination', async function () {
158 await checkBadCountPagination(server.url, path, server.accessToken)
159 })
160
161 it('Should fail with an incorrect sort', async function () {
162 await checkBadSortPagination(server.url, path, server.accessToken)
163 })
164 })
165
166 describe('When blocking a server', function () {
167 it('Should fail with an unauthenticated user', async function () {
168 await makePostBodyRequest({
169 url: server.url,
170 path,
171 fields: { host: 'localhost:9002' },
172 statusCodeExpected: 401
173 })
174 })
175
176 it('Should fail with an unknown server', async function () {
177 await makePostBodyRequest({
178 url: server.url,
179 token: server.accessToken,
180 path,
181 fields: { host: 'localhost:9003' },
182 statusCodeExpected: 404
183 })
184 })
185
186 it('Should fail with our own server', async function () {
187 await makePostBodyRequest({
188 url: server.url,
189 token: server.accessToken,
190 path,
191 fields: { host: 'localhost:9001' },
192 statusCodeExpected: 409
193 })
194 })
195
196 it('Should succeed with the correct params', async function () {
197 await makePostBodyRequest({
198 url: server.url,
199 token: server.accessToken,
200 path,
201 fields: { host: 'localhost:9002' },
202 statusCodeExpected: 204
203 })
204 })
205 })
206
207 describe('When unblocking a server', function () {
208 it('Should fail with an unauthenticated user', async function () {
209 await makeDeleteRequest({
210 url: server.url,
211 path: path + '/localhost:9002',
212 statusCodeExpected: 401
213 })
214 })
215
216 it('Should fail with an unknown server block', async function () {
217 await makeDeleteRequest({
218 url: server.url,
219 path: path + '/localhost:9003',
220 token: server.accessToken,
221 statusCodeExpected: 404
222 })
223 })
224
225 it('Should succeed with the correct params', async function () {
226 await makeDeleteRequest({
227 url: server.url,
228 path: path + '/localhost:9002',
229 token: server.accessToken,
230 statusCodeExpected: 204
231 })
232 })
233 })
234 })
235 })
236
237 describe('When managing server blocklist', function () {
238
239 describe('When managing server accounts blocklist', function () {
240 const path = '/api/v1/server/blocklist/accounts'
241
242 describe('When listing blocked accounts', function () {
243 it('Should fail with an unauthenticated user', async function () {
244 await makeGetRequest({
245 url: server.url,
246 path,
247 statusCodeExpected: 401
248 })
249 })
250
251 it('Should fail with a user without the appropriate rights', async function () {
252 await makeGetRequest({
253 url: server.url,
254 token: userAccessToken,
255 path,
256 statusCodeExpected: 403
257 })
258 })
259
260 it('Should fail with a bad start pagination', async function () {
261 await checkBadStartPagination(server.url, path, server.accessToken)
262 })
263
264 it('Should fail with a bad count pagination', async function () {
265 await checkBadCountPagination(server.url, path, server.accessToken)
266 })
267
268 it('Should fail with an incorrect sort', async function () {
269 await checkBadSortPagination(server.url, path, server.accessToken)
270 })
271 })
272
273 describe('When blocking an account', function () {
274 it('Should fail with an unauthenticated user', async function () {
275 await makePostBodyRequest({
276 url: server.url,
277 path,
278 fields: { accountName: 'user1' },
279 statusCodeExpected: 401
280 })
281 })
282
283 it('Should fail with a user without the appropriate rights', async function () {
284 await makePostBodyRequest({
285 url: server.url,
286 token: userAccessToken,
287 path,
288 fields: { accountName: 'user1' },
289 statusCodeExpected: 403
290 })
291 })
292
293 it('Should fail with an unknown account', async function () {
294 await makePostBodyRequest({
295 url: server.url,
296 token: server.accessToken,
297 path,
298 fields: { accountName: 'user2' },
299 statusCodeExpected: 404
300 })
301 })
302
303 it('Should fail to block ourselves', async function () {
304 await makePostBodyRequest({
305 url: server.url,
306 token: server.accessToken,
307 path,
308 fields: { accountName: 'root' },
309 statusCodeExpected: 409
310 })
311 })
312
313 it('Should succeed with the correct params', async function () {
314 await makePostBodyRequest({
315 url: server.url,
316 token: server.accessToken,
317 path,
318 fields: { accountName: 'user1' },
319 statusCodeExpected: 204
320 })
321 })
322 })
323
324 describe('When unblocking an account', function () {
325 it('Should fail with an unauthenticated user', async function () {
326 await makeDeleteRequest({
327 url: server.url,
328 path: path + '/user1',
329 statusCodeExpected: 401
330 })
331 })
332
333 it('Should fail with a user without the appropriate rights', async function () {
334 await makeDeleteRequest({
335 url: server.url,
336 path: path + '/user1',
337 token: userAccessToken,
338 statusCodeExpected: 403
339 })
340 })
341
342 it('Should fail with an unknown account block', async function () {
343 await makeDeleteRequest({
344 url: server.url,
345 path: path + '/user2',
346 token: server.accessToken,
347 statusCodeExpected: 404
348 })
349 })
350
351 it('Should succeed with the correct params', async function () {
352 await makeDeleteRequest({
353 url: server.url,
354 path: path + '/user1',
355 token: server.accessToken,
356 statusCodeExpected: 204
357 })
358 })
359 })
360 })
361
362 describe('When managing server servers blocklist', function () {
363 const path = '/api/v1/server/blocklist/servers'
364
365 describe('When listing blocked servers', function () {
366 it('Should fail with an unauthenticated user', async function () {
367 await makeGetRequest({
368 url: server.url,
369 path,
370 statusCodeExpected: 401
371 })
372 })
373
374 it('Should fail with a user without the appropriate rights', async function () {
375 await makeGetRequest({
376 url: server.url,
377 token: userAccessToken,
378 path,
379 statusCodeExpected: 403
380 })
381 })
382
383 it('Should fail with a bad start pagination', async function () {
384 await checkBadStartPagination(server.url, path, server.accessToken)
385 })
386
387 it('Should fail with a bad count pagination', async function () {
388 await checkBadCountPagination(server.url, path, server.accessToken)
389 })
390
391 it('Should fail with an incorrect sort', async function () {
392 await checkBadSortPagination(server.url, path, server.accessToken)
393 })
394 })
395
396 describe('When blocking a server', function () {
397 it('Should fail with an unauthenticated user', async function () {
398 await makePostBodyRequest({
399 url: server.url,
400 path,
401 fields: { host: 'localhost:9002' },
402 statusCodeExpected: 401
403 })
404 })
405
406 it('Should fail with a user without the appropriate rights', async function () {
407 await makePostBodyRequest({
408 url: server.url,
409 token: userAccessToken,
410 path,
411 fields: { host: 'localhost:9002' },
412 statusCodeExpected: 403
413 })
414 })
415
416 it('Should fail with an unknown server', async function () {
417 await makePostBodyRequest({
418 url: server.url,
419 token: server.accessToken,
420 path,
421 fields: { host: 'localhost:9003' },
422 statusCodeExpected: 404
423 })
424 })
425
426 it('Should fail with our own server', async function () {
427 await makePostBodyRequest({
428 url: server.url,
429 token: server.accessToken,
430 path,
431 fields: { host: 'localhost:9001' },
432 statusCodeExpected: 409
433 })
434 })
435
436 it('Should succeed with the correct params', async function () {
437 await makePostBodyRequest({
438 url: server.url,
439 token: server.accessToken,
440 path,
441 fields: { host: 'localhost:9002' },
442 statusCodeExpected: 204
443 })
444 })
445 })
446
447 describe('When unblocking a server', function () {
448 it('Should fail with an unauthenticated user', async function () {
449 await makeDeleteRequest({
450 url: server.url,
451 path: path + '/localhost:9002',
452 statusCodeExpected: 401
453 })
454 })
455
456 it('Should fail with a user without the appropriate rights', async function () {
457 await makeDeleteRequest({
458 url: server.url,
459 path: path + '/localhost:9002',
460 token: userAccessToken,
461 statusCodeExpected: 403
462 })
463 })
464
465 it('Should fail with an unknown server block', async function () {
466 await makeDeleteRequest({
467 url: server.url,
468 path: path + '/localhost:9003',
469 token: server.accessToken,
470 statusCodeExpected: 404
471 })
472 })
473
474 it('Should succeed with the correct params', async function () {
475 await makeDeleteRequest({
476 url: server.url,
477 path: path + '/localhost:9002',
478 token: server.accessToken,
479 statusCodeExpected: 204
480 })
481 })
482 })
483 })
484 })
485
486 after(async function () {
487 killallServers(servers)
488
489 // Keep the logs if the test failed
490 if (this['ok']) {
491 await flushTests()
492 }
493 })
494})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 44460a167..877ceb0a7 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,5 +1,6 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './accounts' 2import './accounts'
3import './blocklist'
3import './config' 4import './config'
4import './follows' 5import './follows'
5import './jobs' 6import './jobs'
@@ -15,3 +16,5 @@ import './video-channels'
15import './video-comments' 16import './video-comments'
16import './video-imports' 17import './video-imports'
17import './videos' 18import './videos'
19import './videos-filter'
20import './videos-history'
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index cbfa0c137..ec46609a4 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -315,7 +315,7 @@ describe('Test users API validators', function () {
315 315
316 it('Should fail with a too long description', async function () { 316 it('Should fail with a too long description', async function () {
317 const fields = { 317 const fields = {
318 description: 'super'.repeat(60) 318 description: 'super'.repeat(201)
319 } 319 }
320 320
321 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 321 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 3a7942945..e5696224d 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -118,12 +118,12 @@ describe('Test video channels API validator', function () {
118 }) 118 })
119 119
120 it('Should fail with a long description', async function () { 120 it('Should fail with a long description', async function () {
121 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) 121 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) })
122 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) 122 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
123 }) 123 })
124 124
125 it('Should fail with a long support text', async function () { 125 it('Should fail with a long support text', async function () {
126 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) 126 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
127 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) 127 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
128 }) 128 })
129 129
@@ -185,12 +185,12 @@ describe('Test video channels API validator', function () {
185 }) 185 })
186 186
187 it('Should fail with a long description', async function () { 187 it('Should fail with a long description', async function () {
188 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(150) }) 188 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(201) })
189 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 189 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
190 }) 190 })
191 191
192 it('Should fail with a long support text', async function () { 192 it('Should fail with a long support text', async function () {
193 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) 193 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
194 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 194 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
195 }) 195 })
196 196
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts
index 44645b0e2..b51f3d2cd 100644
--- a/server/tests/api/check-params/video-imports.ts
+++ b/server/tests/api/check-params/video-imports.ts
@@ -140,7 +140,7 @@ describe('Test video imports API validator', function () {
140 }) 140 })
141 141
142 it('Should fail with a long support text', async function () { 142 it('Should fail with a long support text', async function () {
143 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) 143 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
144 144
145 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 145 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
146 }) 146 })
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
new file mode 100644
index 000000000..784cd8ba1
--- /dev/null
+++ b/server/tests/api/check-params/videos-filter.ts
@@ -0,0 +1,127 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 createUser,
7 flushTests,
8 killallServers,
9 makeGetRequest,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 userLogin
14} from '../../utils'
15import { UserRole } from '../../../../shared/models/users'
16
17const expect = chai.expect
18
19async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
20 const paths = [
21 '/api/v1/video-channels/root_channel/videos',
22 '/api/v1/accounts/root/videos',
23 '/api/v1/videos',
24 '/api/v1/search/videos'
25 ]
26
27 for (const path of paths) {
28 await makeGetRequest({
29 url: server.url,
30 path,
31 token,
32 query: {
33 filter
34 },
35 statusCodeExpected
36 })
37 }
38}
39
40describe('Test videos filters', function () {
41 let server: ServerInfo
42 let userAccessToken: string
43 let moderatorAccessToken: string
44
45 // ---------------------------------------------------------------
46
47 before(async function () {
48 this.timeout(30000)
49
50 await flushTests()
51
52 server = await runServer(1)
53
54 await setAccessTokensToServers([ server ])
55
56 const user = { username: 'user1', password: 'my super password' }
57 await createUser(server.url, server.accessToken, user.username, user.password)
58 userAccessToken = await userLogin(server, user)
59
60 const moderator = { username: 'moderator', password: 'my super password' }
61 await createUser(
62 server.url,
63 server.accessToken,
64 moderator.username,
65 moderator.password,
66 undefined,
67 undefined,
68 UserRole.MODERATOR
69 )
70 moderatorAccessToken = await userLogin(server, moderator)
71 })
72
73 describe('When setting a video filter', function () {
74
75 it('Should fail with a bad filter', async function () {
76 await testEndpoints(server, server.accessToken, 'bad-filter', 400)
77 })
78
79 it('Should succeed with a good filter', async function () {
80 await testEndpoints(server, server.accessToken,'local', 200)
81 })
82
83 it('Should fail to list all-local with a simple user', async function () {
84 await testEndpoints(server, userAccessToken, 'all-local', 401)
85 })
86
87 it('Should succeed to list all-local with a moderator', async function () {
88 await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
89 })
90
91 it('Should succeed to list all-local with an admin', async function () {
92 await testEndpoints(server, server.accessToken, 'all-local', 200)
93 })
94
95 // Because we cannot authenticate the user on the RSS endpoint
96 it('Should fail on the feeds endpoint with the all-local filter', async function () {
97 await makeGetRequest({
98 url: server.url,
99 path: '/feeds/videos.json',
100 statusCodeExpected: 401,
101 query: {
102 filter: 'all-local'
103 }
104 })
105 })
106
107 it('Should succed on the feeds endpoint with the local filter', async function () {
108 await makeGetRequest({
109 url: server.url,
110 path: '/feeds/videos.json',
111 statusCodeExpected: 200,
112 query: {
113 filter: 'local'
114 }
115 })
116 })
117 })
118
119 after(async function () {
120 killallServers([ server ])
121
122 // Keep the logs if the test failed
123 if (this['ok']) {
124 await flushTests()
125 }
126 })
127})
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
new file mode 100644
index 000000000..808c3b616
--- /dev/null
+++ b/server/tests/api/check-params/videos-history.ts
@@ -0,0 +1,79 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 killallServers,
8 makePostBodyRequest,
9 makePutBodyRequest,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15
16const expect = chai.expect
17
18describe('Test videos history API validator', function () {
19 let path: string
20 let server: ServerInfo
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(30000)
26
27 await flushTests()
28
29 server = await runServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 const res = await uploadVideo(server.url, server.accessToken, {})
34 const videoUUID = res.body.video.uuid
35
36 path = '/api/v1/videos/' + videoUUID + '/watching'
37 })
38
39 describe('When notifying a user is watching a video', function () {
40
41 it('Should fail with an unauthenticated user', async function () {
42 const fields = { currentTime: 5 }
43 await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 })
44 })
45
46 it('Should fail with an incorrect video id', async function () {
47 const fields = { currentTime: 5 }
48 const path = '/api/v1/videos/blabla/watching'
49 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
50 })
51
52 it('Should fail with an unknown video', async function () {
53 const fields = { currentTime: 5 }
54 const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching'
55
56 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 404 })
57 })
58
59 it('Should fail with a bad current time', async function () {
60 const fields = { currentTime: 'hello' }
61 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
62 })
63
64 it('Should succeed with the correct parameters', async function () {
65 const fields = { currentTime: 5 }
66
67 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 })
68 })
69 })
70
71 after(async function () {
72 killallServers([ server ])
73
74 // Keep the logs if the test failed
75 if (this['ok']) {
76 await flushTests()
77 }
78 })
79})
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 904d22870..699f135c7 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -233,7 +233,7 @@ describe('Test videos API validator', function () {
233 }) 233 })
234 234
235 it('Should fail with a long support text', async function () { 235 it('Should fail with a long support text', async function () {
236 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) 236 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
237 const attaches = baseCorrectAttaches 237 const attaches = baseCorrectAttaches
238 238
239 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 239 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -482,7 +482,7 @@ describe('Test videos API validator', function () {
482 }) 482 })
483 483
484 it('Should fail with a long support text', async function () { 484 it('Should fail with a long support text', async function () {
485 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(150) }) 485 const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) })
486 486
487 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) 487 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
488 }) 488 })
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts
new file mode 100644
index 000000000..8e69b95a6
--- /dev/null
+++ b/server/tests/api/index-4.ts
@@ -0,0 +1 @@
import './redundancy'
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index 2d996dbf9..bc140f860 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -2,3 +2,4 @@
2import './index-1' 2import './index-1'
3import './index-2' 3import './index-2'
4import './index-3' 4import './index-3'
5import './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
3import * as chai from 'chai'
4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos'
6import {
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'
22import { waitJobs } from '../../utils/server/jobs'
23import * as magnetUtil from 'magnet-uri'
24import { updateRedundancy } from '../../utils/server/redundancy'
25import { ActorFollow } from '../../../../shared/models/actors'
26import { readdir } from 'fs-extra'
27import { join } from 'path'
28import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
29import { getStats } from '../../utils/server/stats'
30import { ServerStats } from '../../../../shared/models/server/server-stats.model'
31
32const expect = chai.expect
33
34let servers: ServerInfo[] = []
35let video1Server2UUID: string
36
37function 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
48async 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
87async 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
106async 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
120async 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
134async 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
172async 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
187async 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
202async function cleanServers () {
203 killallServers(servers)
204}
205
206describe('Test videos redundancy', function () {
207
208 describe('With most-views strategy', function () {
209 const strategy = 'most-views'
210
211 before(function () {
212 this.timeout(120000)
213
214 return runServers(strategy)
215 })
216
217 it('Should have 1 webseed on the first video', async function () {
218 await check1WebSeed(strategy)
219 await checkStatsWith1Webseed(strategy)
220 })
221
222 it('Should enable redundancy on server 1', function () {
223 return enableRedundancyOnServer1()
224 })
225
226 it('Should have 2 webseed on the first video', async function () {
227 this.timeout(40000)
228
229 await waitJobs(servers)
230 await waitUntilLog(servers[0], 'Duplicated ', 4)
231 await waitJobs(servers)
232
233 await check2Webseeds(strategy)
234 await checkStatsWith2Webseed(strategy)
235 })
236
237 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
238 this.timeout(40000)
239
240 await disableRedundancyOnServer1()
241
242 await waitJobs(servers)
243 await wait(5000)
244
245 await check1WebSeed(strategy)
246
247 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
248 })
249
250 after(function () {
251 return cleanServers()
252 })
253 })
254
255 describe('With trending strategy', function () {
256 const strategy = 'trending'
257
258 before(function () {
259 this.timeout(120000)
260
261 return runServers(strategy)
262 })
263
264 it('Should have 1 webseed on the first video', async function () {
265 await check1WebSeed(strategy)
266 await checkStatsWith1Webseed(strategy)
267 })
268
269 it('Should enable redundancy on server 1', function () {
270 return enableRedundancyOnServer1()
271 })
272
273 it('Should have 2 webseed on the first video', async function () {
274 this.timeout(40000)
275
276 await waitJobs(servers)
277 await waitUntilLog(servers[0], 'Duplicated ', 4)
278 await waitJobs(servers)
279
280 await check2Webseeds(strategy)
281 await checkStatsWith2Webseed(strategy)
282 })
283
284 it('Should unfollow on server 1 and remove duplicated videos', async function () {
285 this.timeout(40000)
286
287 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
288
289 await waitJobs(servers)
290 await wait(5000)
291
292 await check1WebSeed(strategy)
293
294 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
295 })
296
297 after(function () {
298 return cleanServers()
299 })
300 })
301
302 describe('With recently added strategy', function () {
303 const strategy = 'recently-added'
304
305 before(function () {
306 this.timeout(120000)
307
308 return runServers(strategy, { min_views: 3 })
309 })
310
311 it('Should have 1 webseed on the first video', async function () {
312 await check1WebSeed(strategy)
313 await checkStatsWith1Webseed(strategy)
314 })
315
316 it('Should enable redundancy on server 1', function () {
317 return enableRedundancyOnServer1()
318 })
319
320 it('Should still have 1 webseed on the first video', async function () {
321 this.timeout(40000)
322
323 await waitJobs(servers)
324 await wait(15000)
325 await waitJobs(servers)
326
327 await check1WebSeed(strategy)
328 await checkStatsWith1Webseed(strategy)
329 })
330
331 it('Should view 2 times the first video to have > min_views config', async function () {
332 this.timeout(40000)
333
334 await viewVideo(servers[ 0 ].url, video1Server2UUID)
335 await viewVideo(servers[ 2 ].url, video1Server2UUID)
336
337 await wait(10000)
338 await waitJobs(servers)
339 })
340
341 it('Should have 2 webseed on the first video', async function () {
342 this.timeout(40000)
343
344 await waitJobs(servers)
345 await waitUntilLog(servers[0], 'Duplicated ', 4)
346 await waitJobs(servers)
347
348 await check2Webseeds(strategy)
349 await checkStatsWith2Webseed(strategy)
350 })
351
352 it('Should remove the video and the redundancy files', async function () {
353 this.timeout(20000)
354
355 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
356
357 await waitJobs(servers)
358
359 for (const server of servers) {
360 await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber)
361 }
362 })
363
364 after(function () {
365 return cleanServers()
366 })
367 })
368
369 describe('Test expiration', function () {
370 const strategy = 'recently-added'
371
372 async function checkContains (servers: ServerInfo[], str: string) {
373 for (const server of servers) {
374 const res = await getVideo(server.url, video1Server2UUID)
375 const video: VideoDetails = res.body
376
377 for (const f of video.files) {
378 expect(f.magnetUri).to.contain(str)
379 }
380 }
381 }
382
383 async function checkNotContains (servers: ServerInfo[], str: string) {
384 for (const server of servers) {
385 const res = await getVideo(server.url, video1Server2UUID)
386 const video: VideoDetails = res.body
387
388 for (const f of video.files) {
389 expect(f.magnetUri).to.not.contain(str)
390 }
391 }
392 }
393
394 before(async function () {
395 this.timeout(120000)
396
397 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
398
399 await enableRedundancyOnServer1()
400 })
401
402 it('Should still have 2 webseeds after 10 seconds', async function () {
403 this.timeout(40000)
404
405 await wait(10000)
406
407 try {
408 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
409 } catch {
410 // Maybe a server deleted a redundancy in the scheduler
411 await wait(2000)
412
413 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
414 }
415 })
416
417 it('Should stop server 1 and expire video redundancy', async function () {
418 this.timeout(40000)
419
420 killallServers([ servers[0] ])
421
422 await wait(10000)
423
424 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
425 })
426
427 after(function () {
428 return killallServers([ servers[1], servers[2] ])
429 })
430 })
431
432 describe('Test file replacement', function () {
433 let video2Server2UUID: string
434 const strategy = 'recently-added'
435
436 before(async function () {
437 this.timeout(120000)
438
439 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
440
441 await enableRedundancyOnServer1()
442
443 await waitJobs(servers)
444 await waitUntilLog(servers[0], 'Duplicated ', 4)
445 await waitJobs(servers)
446
447 await check2Webseeds(strategy)
448 await checkStatsWith2Webseed(strategy)
449
450 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
451 video2Server2UUID = res.body.video.uuid
452 })
453
454 it('Should cache video 2 webseed on the first video', async function () {
455 this.timeout(50000)
456
457 await waitJobs(servers)
458
459 await wait(7000)
460
461 try {
462 await check1WebSeed(strategy, video1Server2UUID)
463 await check2Webseeds(strategy, video2Server2UUID)
464 } catch {
465 await wait(3000)
466
467 try {
468 await check1WebSeed(strategy, video1Server2UUID)
469 await check2Webseeds(strategy, video2Server2UUID)
470 } catch {
471 await wait(5000)
472
473 await check1WebSeed(strategy, video1Server2UUID)
474 await check2Webseeds(strategy, video2Server2UUID)
475 }
476 }
477 })
478
479 after(function () {
480 return cleanServers()
481 })
482 })
483})
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index 310c291bf..e80e93e7f 100644
--- a/server/tests/api/server/follows.ts
+++ b/server/tests/api/server/follows.ts
@@ -93,7 +93,26 @@ describe('Test follows', function () {
93 expect(server3Follow.state).to.equal('accepted') 93 expect(server3Follow.state).to.equal('accepted')
94 }) 94 })
95 95
96 it('Should have 0 followings on server 1 and 2', async function () { 96 it('Should search followings on server 1', async function () {
97 {
98 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':9002')
99 const follows = res.body.data
100
101 expect(res.body.total).to.equal(1)
102 expect(follows.length).to.equal(1)
103 expect(follows[ 0 ].following.host).to.equal('localhost:9002')
104 }
105
106 {
107 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', 'bla')
108 const follows = res.body.data
109
110 expect(res.body.total).to.equal(0)
111 expect(follows.length).to.equal(0)
112 }
113 })
114
115 it('Should have 0 followings on server 2 and 3', async function () {
97 for (const server of [ servers[1], servers[2] ]) { 116 for (const server of [ servers[1], servers[2] ]) {
98 const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') 117 const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt')
99 const follows = res.body.data 118 const follows = res.body.data
@@ -116,6 +135,25 @@ describe('Test follows', function () {
116 } 135 }
117 }) 136 })
118 137
138 it('Should search followers on server 2', async function () {
139 {
140 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', '9001')
141 const follows = res.body.data
142
143 expect(res.body.total).to.equal(1)
144 expect(follows.length).to.equal(1)
145 expect(follows[ 0 ].following.host).to.equal('localhost:9003')
146 }
147
148 {
149 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', 'bla')
150 const follows = res.body.data
151
152 expect(res.body.total).to.equal(0)
153 expect(follows.length).to.equal(0)
154 }
155 })
156
119 it('Should have 0 followers on server 1', async function () { 157 it('Should have 0 followers on server 1', async function () {
120 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') 158 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
121 const follows = res.body.data 159 const follows = res.body.data
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index c74c68a33..eeb8b7a28 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -3,7 +3,6 @@ import './email'
3import './follows' 3import './follows'
4import './handle-down' 4import './handle-down'
5import './jobs' 5import './jobs'
6import './redundancy'
7import './reverse-proxy' 6import './reverse-proxy'
8import './stats' 7import './stats'
9import './tracker' 8import './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
3import * as chai from 'chai'
4import 'mocha'
5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index'
6import {
7 createUser,
8 doubleFollow,
9 flushAndRunMultipleServers,
10 flushTests,
11 killallServers,
12 ServerInfo,
13 uploadVideo,
14 userLogin
15} from '../../utils/index'
16import { setAccessTokensToServers } from '../../utils/users/login'
17import { getVideosListWithToken, getVideosList } from '../../utils/videos/videos'
18import {
19 addVideoCommentReply,
20 addVideoCommentThread,
21 getVideoCommentThreads,
22 getVideoThreadComments
23} from '../../utils/videos/video-comments'
24import { waitJobs } from '../../utils/server/jobs'
25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
26import {
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
41const expect = chai.expect
42
43async 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
57async 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
71describe('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 @@
1import './blocklist'
1import './user-subscriptions' 2import './user-subscriptions'
2import './users' 3import './users'
3import './users-verification' 4import './users-verification'
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index b67072851..d8699db17 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -148,6 +148,12 @@ describe('Test users with multiple servers', function () {
148 expect(rootServer1Get.displayName).to.equal('my super display name') 148 expect(rootServer1Get.displayName).to.equal('my super display name')
149 expect(rootServer1Get.description).to.equal('my super description updated') 149 expect(rootServer1Get.description).to.equal('my super description updated')
150 150
151 if (server.serverNumber === 1) {
152 expect(rootServer1Get.userId).to.be.a('number')
153 } else {
154 expect(rootServer1Get.userId).to.be.undefined
155 }
156
151 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') 157 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
152 } 158 }
153 }) 159 })
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 8b9c6b455..513bca8a0 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -180,7 +180,7 @@ describe('Test users', function () {
180 it('Should be able to upload a video again') 180 it('Should be able to upload a video again')
181 181
182 it('Should be able to create a new user', async function () { 182 it('Should be able to create a new user', async function () {
183 await createUser(server.url, accessToken, user.username,user.password, 2 * 1024 * 1024) 183 await createUser(server.url, accessToken, user.username, user.password, 2 * 1024 * 1024)
184 }) 184 })
185 185
186 it('Should be able to login with this user', async function () { 186 it('Should be able to login with this user', async function () {
@@ -322,6 +322,40 @@ describe('Test users', function () {
322 expect(users[ 1 ].nsfwPolicy).to.equal('display') 322 expect(users[ 1 ].nsfwPolicy).to.equal('display')
323 }) 323 })
324 324
325 it('Should search user by username', async function () {
326 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot')
327 const users = res.body.data as User[]
328
329 expect(res.body.total).to.equal(1)
330 expect(users.length).to.equal(1)
331
332 expect(users[ 0 ].username).to.equal('root')
333 })
334
335 it('Should search user by email', async function () {
336 {
337 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam')
338 const users = res.body.data as User[]
339
340 expect(res.body.total).to.equal(1)
341 expect(users.length).to.equal(1)
342
343 expect(users[ 0 ].username).to.equal('user_1')
344 expect(users[ 0 ].email).to.equal('user_1@example.com')
345 }
346
347 {
348 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example')
349 const users = res.body.data as User[]
350
351 expect(res.body.total).to.equal(2)
352 expect(users.length).to.equal(2)
353
354 expect(users[ 0 ].username).to.equal('root')
355 expect(users[ 1 ].username).to.equal('user_1')
356 }
357 })
358
325 it('Should update my password', async function () { 359 it('Should update my password', async function () {
326 await updateMyUser({ 360 await updateMyUser({
327 url: server.url, 361 url: server.url,
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index bf58f9c79..9bdb78491 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -14,4 +14,6 @@ import './video-nsfw'
14import './video-privacy' 14import './video-privacy'
15import './video-schedule-update' 15import './video-schedule-update'
16import './video-transcoder' 16import './video-transcoder'
17import './videos-filter'
18import './videos-history'
17import './videos-overview' 19import './videos-overview'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 4553ee855..b9ace2885 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -987,19 +987,19 @@ describe('Test multiple servers', function () {
987 files: [ 987 files: [
988 { 988 {
989 resolution: 720, 989 resolution: 720,
990 size: 36000 990 size: 72000
991 }, 991 },
992 { 992 {
993 resolution: 480, 993 resolution: 480,
994 size: 21000 994 size: 45000
995 }, 995 },
996 { 996 {
997 resolution: 360, 997 resolution: 360,
998 size: 17000 998 size: 34600
999 }, 999 },
1000 { 1000 {
1001 resolution: 240, 1001 resolution: 240,
1002 size: 13000 1002 size: 24770
1003 } 1003 }
1004 ] 1004 ]
1005 } 1005 }
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index e3d62b7a0..089c3df25 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -118,7 +118,7 @@ describe('Test a single server', function () {
118 const categories = res.body 118 const categories = res.body
119 expect(Object.keys(categories)).to.have.length.above(10) 119 expect(Object.keys(categories)).to.have.length.above(10)
120 120
121 expect(categories[11]).to.equal('News') 121 expect(categories[11]).to.equal('News & Politics')
122 }) 122 })
123 123
124 it('Should list video licences', async function () { 124 it('Should list video licences', async function () {
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index b7866d529..aaee79a4a 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -30,7 +30,7 @@ describe('Test video imports', function () {
30 const videoHttp: VideoDetails = resHttp.body 30 const videoHttp: VideoDetails = resHttp.body
31 31
32 expect(videoHttp.name).to.equal('small video - youtube') 32 expect(videoHttp.name).to.equal('small video - youtube')
33 expect(videoHttp.category.label).to.equal('News') 33 expect(videoHttp.category.label).to.equal('News & Politics')
34 expect(videoHttp.licence.label).to.equal('Attribution') 34 expect(videoHttp.licence.label).to.equal('Attribution')
35 expect(videoHttp.language.label).to.equal('Unknown') 35 expect(videoHttp.language.label).to.equal('Unknown')
36 expect(videoHttp.nsfw).to.be.false 36 expect(videoHttp.nsfw).to.be.false
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 0f83d4d57..85795d2ed 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -4,8 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import * as ffmpeg from 'fluent-ffmpeg' 6import * as ffmpeg from 'fluent-ffmpeg'
7import { VideoDetails, VideoState } from '../../../../shared/models/videos' 7import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
8import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' 8import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
9import { 9import {
10 buildAbsoluteFixturePath, 10 buildAbsoluteFixturePath,
11 doubleFollow, 11 doubleFollow,
@@ -18,10 +18,13 @@ import {
18 ServerInfo, 18 ServerInfo,
19 setAccessTokensToServers, 19 setAccessTokensToServers,
20 uploadVideo, 20 uploadVideo,
21 webtorrentAdd 21 webtorrentAdd,
22 generateHighBitrateVideo
22} from '../../utils' 23} from '../../utils'
23import { join } from 'path' 24import { join } from 'path'
24import { waitJobs } from '../../utils/server/jobs' 25import { waitJobs } from '../../utils/server/jobs'
26import { pathExists } from 'fs-extra'
27import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
25 28
26const expect = chai.expect 29const expect = chai.expect
27 30
@@ -121,7 +124,7 @@ describe('Test video transcoding', function () {
121 expect(videoDetails.files).to.have.lengthOf(4) 124 expect(videoDetails.files).to.have.lengthOf(4)
122 125
123 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 126 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
124 const probe = await audio.get(ffmpeg, path) 127 const probe = await audio.get(path)
125 128
126 if (probe.audioStream) { 129 if (probe.audioStream) {
127 expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') 130 expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac')
@@ -152,7 +155,7 @@ describe('Test video transcoding', function () {
152 155
153 expect(videoDetails.files).to.have.lengthOf(4) 156 expect(videoDetails.files).to.have.lengthOf(4)
154 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 157 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
155 const probe = await audio.get(ffmpeg, path) 158 const probe = await audio.get(path)
156 expect(probe).to.not.have.property('audioStream') 159 expect(probe).to.not.have.property('audioStream')
157 } 160 }
158 }) 161 })
@@ -177,9 +180,9 @@ describe('Test video transcoding', function () {
177 180
178 expect(videoDetails.files).to.have.lengthOf(4) 181 expect(videoDetails.files).to.have.lengthOf(4)
179 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) 182 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
180 const fixtureVideoProbe = await audio.get(ffmpeg, fixturePath) 183 const fixtureVideoProbe = await audio.get(fixturePath)
181 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 184 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4')
182 const videoProbe = await audio.get(ffmpeg, path) 185 const videoProbe = await audio.get(path)
183 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { 186 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
184 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] 187 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
185 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) 188 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
@@ -228,7 +231,7 @@ describe('Test video transcoding', function () {
228 } 231 }
229 }) 232 })
230 233
231 it('Should wait transcoding before publishing the video', async function () { 234 it('Should wait for transcoding before publishing the video', async function () {
232 this.timeout(80000) 235 this.timeout(80000)
233 236
234 { 237 {
@@ -281,6 +284,45 @@ describe('Test video transcoding', function () {
281 } 284 }
282 }) 285 })
283 286
287 it('Should respect maximum bitrate values', async function () {
288 this.timeout(160000)
289
290 let tempFixturePath: string
291
292 {
293 tempFixturePath = await generateHighBitrateVideo()
294
295 const bitrate = await getVideoFileBitrate(tempFixturePath)
296 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
297 }
298
299 const videoAttributes = {
300 name: 'high bitrate video',
301 description: 'high bitrate video',
302 fixture: tempFixturePath
303 }
304
305 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
306
307 await waitJobs(servers)
308
309 for (const server of servers) {
310 const res = await getVideosList(server.url)
311
312 const video = res.body.data.find(v => v.name === videoAttributes.name)
313
314 for (const resolution of ['240', '360', '480', '720', '1080']) {
315 const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
316 const bitrate = await getVideoFileBitrate(path)
317 const fps = await getVideoFileFPS(path)
318 const resolution2 = await getVideoFileResolution(path)
319
320 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
321 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
322 }
323 }
324 })
325
284 after(async function () { 326 after(async function () {
285 killallServers(servers) 327 killallServers(servers)
286 }) 328 })
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
new file mode 100644
index 000000000..a7588129f
--- /dev/null
+++ b/server/tests/api/videos/videos-filter.ts
@@ -0,0 +1,130 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeGetRequest,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo,
15 userLogin
16} from '../../utils'
17import { Video, VideoPrivacy } from '../../../../shared/models/videos'
18import { UserRole } from '../../../../shared/models/users'
19
20const expect = chai.expect
21
22async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) {
23 const paths = [
24 '/api/v1/video-channels/root_channel/videos',
25 '/api/v1/accounts/root/videos',
26 '/api/v1/videos',
27 '/api/v1/search/videos'
28 ]
29
30 const videosResults: Video[][] = []
31
32 for (const path of paths) {
33 const res = await makeGetRequest({
34 url: server.url,
35 path,
36 token,
37 query: {
38 sort: 'createdAt',
39 filter
40 },
41 statusCodeExpected
42 })
43
44 videosResults.push(res.body.data.map(v => v.name))
45 }
46
47 return videosResults
48}
49
50describe('Test videos filter validator', function () {
51 let servers: ServerInfo[]
52
53 // ---------------------------------------------------------------
54
55 before(async function () {
56 this.timeout(120000)
57
58 await flushTests()
59
60 servers = await flushAndRunMultipleServers(2)
61
62 await setAccessTokensToServers(servers)
63
64 for (const server of servers) {
65 const moderator = { username: 'moderator', password: 'my super password' }
66 await createUser(
67 server.url,
68 server.accessToken,
69 moderator.username,
70 moderator.password,
71 undefined,
72 undefined,
73 UserRole.MODERATOR
74 )
75 server['moderatorAccessToken'] = await userLogin(server, moderator)
76
77 await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber })
78
79 {
80 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
81 await uploadVideo(server.url, server.accessToken, attributes)
82 }
83
84 {
85 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
86 await uploadVideo(server.url, server.accessToken, attributes)
87 }
88 }
89
90 await doubleFollow(servers[0], servers[1])
91 })
92
93 describe('Check videos filter', function () {
94
95 it('Should display local videos', async function () {
96 for (const server of servers) {
97 const namesResults = await getVideosNames(server, server.accessToken, 'local')
98 for (const names of namesResults) {
99 expect(names).to.have.lengthOf(1)
100 expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
101 }
102 }
103 })
104
105 it('Should display all local videos by the admin or the moderator', async function () {
106 for (const server of servers) {
107 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
108
109 const namesResults = await getVideosNames(server, token, 'all-local')
110 for (const names of namesResults) {
111 expect(names).to.have.lengthOf(3)
112
113 expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
114 expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber)
115 expect(names[ 2 ]).to.equal('private ' + server.serverNumber)
116 }
117 }
118 }
119 })
120 })
121
122 after(async function () {
123 killallServers(servers)
124
125 // Keep the logs if the test failed
126 if (this['ok']) {
127 await flushTests()
128 }
129 })
130})
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
new file mode 100644
index 000000000..6d289b288
--- /dev/null
+++ b/server/tests/api/videos/videos-history.ts
@@ -0,0 +1,128 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 getVideosListWithToken,
8 getVideoWithToken,
9 killallServers, makePutBodyRequest,
10 runServer, searchVideoWithToken,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15import { Video, VideoDetails } from '../../../../shared/models/videos'
16import { userWatchVideo } from '../../utils/videos/video-history'
17
18const expect = chai.expect
19
20describe('Test videos history', function () {
21 let server: ServerInfo = null
22 let video1UUID: string
23 let video2UUID: string
24 let video3UUID: string
25
26 before(async function () {
27 this.timeout(30000)
28
29 await flushTests()
30
31 server = await runServer(1)
32
33 await setAccessTokensToServers([ server ])
34
35 {
36 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
37 video1UUID = res.body.video.uuid
38 }
39
40 {
41 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
42 video2UUID = res.body.video.uuid
43 }
44
45 {
46 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
47 video3UUID = res.body.video.uuid
48 }
49 })
50
51 it('Should get videos, without watching history', async function () {
52 const res = await getVideosListWithToken(server.url, server.accessToken)
53 const videos: Video[] = res.body.data
54
55 for (const video of videos) {
56 const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id)
57 const videoDetails: VideoDetails = resDetail.body
58
59 expect(video.userHistory).to.be.undefined
60 expect(videoDetails.userHistory).to.be.undefined
61 }
62 })
63
64 it('Should watch the first and second video', async function () {
65 await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
66 await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
67 })
68
69 it('Should return the correct history when listing, searching and getting videos', async function () {
70 const videosOfVideos: Video[][] = []
71
72 {
73 const res = await getVideosListWithToken(server.url, server.accessToken)
74 videosOfVideos.push(res.body.data)
75 }
76
77 {
78 const res = await searchVideoWithToken(server.url, 'video', server.accessToken)
79 videosOfVideos.push(res.body.data)
80 }
81
82 for (const videos of videosOfVideos) {
83 const video1 = videos.find(v => v.uuid === video1UUID)
84 const video2 = videos.find(v => v.uuid === video2UUID)
85 const video3 = videos.find(v => v.uuid === video3UUID)
86
87 expect(video1.userHistory).to.not.be.undefined
88 expect(video1.userHistory.currentTime).to.equal(3)
89
90 expect(video2.userHistory).to.not.be.undefined
91 expect(video2.userHistory.currentTime).to.equal(8)
92
93 expect(video3.userHistory).to.be.undefined
94 }
95
96 {
97 const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID)
98 const videoDetails: VideoDetails = resDetail.body
99
100 expect(videoDetails.userHistory).to.not.be.undefined
101 expect(videoDetails.userHistory.currentTime).to.equal(3)
102 }
103
104 {
105 const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID)
106 const videoDetails: VideoDetails = resDetail.body
107
108 expect(videoDetails.userHistory).to.not.be.undefined
109 expect(videoDetails.userHistory.currentTime).to.equal(8)
110 }
111
112 {
113 const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID)
114 const videoDetails: VideoDetails = resDetail.body
115
116 expect(videoDetails.userHistory).to.be.undefined
117 }
118 })
119
120 after(async function () {
121 killallServers([ server ])
122
123 // Keep the logs if the test failed
124 if (this['ok']) {
125 await flushTests()
126 }
127 })
128})
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts
new file mode 100644
index 000000000..66dd39cce
--- /dev/null
+++ b/server/tests/cli/optimize-old-videos.ts
@@ -0,0 +1,120 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import * as chai from 'chai'
5import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos'
6import {
7 doubleFollow,
8 execCLI,
9 flushAndRunMultipleServers,
10 flushTests, generateHighBitrateVideo,
11 getEnvCli,
12 getVideo,
13 getVideosList,
14 killallServers, root,
15 ServerInfo,
16 setAccessTokensToServers,
17 uploadVideo, viewVideo, wait
18} from '../utils'
19import { waitJobs } from '../utils/server/jobs'
20import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
21import { VIDEO_TRANSCODING_FPS } from '../../initializers'
22import { join } from 'path'
23
24const expect = chai.expect
25
26describe('Test optimize old videos', function () {
27 let servers: ServerInfo[] = []
28 let video1UUID: string
29 let video2UUID: string
30
31 before(async function () {
32 this.timeout(200000)
33
34 await flushTests()
35
36 // Run server 2 to have transcoding enabled
37 servers = await flushAndRunMultipleServers(2)
38 await setAccessTokensToServers(servers)
39
40 await doubleFollow(servers[0], servers[1])
41
42 let tempFixturePath: string
43
44 {
45 tempFixturePath = await generateHighBitrateVideo()
46
47 const bitrate = await getVideoFileBitrate(tempFixturePath)
48 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
49 }
50
51 // Upload two videos for our needs
52 const res1 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video1', fixture: tempFixturePath })
53 video1UUID = res1.body.video.uuid
54 const res2 = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video2', fixture: tempFixturePath })
55 video2UUID = res2.body.video.uuid
56
57 await waitJobs(servers)
58 })
59
60 it('Should have two video files on each server', async function () {
61 this.timeout(30000)
62
63 for (const server of servers) {
64 const res = await getVideosList(server.url)
65 const videos = res.body.data
66 expect(videos).to.have.lengthOf(2)
67
68 for (const video of videos) {
69 const res2 = await getVideo(server.url, video.uuid)
70 const videoDetail: VideoDetails = res2.body
71 expect(videoDetail.files).to.have.lengthOf(1)
72 }
73 }
74 })
75
76 it('Should run optimize script', async function () {
77 this.timeout(120000)
78
79 const env = getEnvCli(servers[0])
80 await execCLI(`${env} npm run optimize-old-videos`)
81
82 await waitJobs(servers)
83
84 for (const server of servers) {
85 const res = await getVideosList(server.url)
86 const videos: Video[] = res.body.data
87
88 expect(videos).to.have.lengthOf(2)
89
90 for (const video of videos) {
91 await viewVideo(server.url, video.uuid)
92
93 // Refresh video
94 await waitJobs(servers)
95 await wait(5000)
96 await waitJobs(servers)
97
98 const res2 = await getVideo(server.url, video.uuid)
99 const videosDetails: VideoDetails = res2.body
100
101 expect(videosDetails.files).to.have.lengthOf(1)
102 const file = videosDetails.files[0]
103
104 expect(file.size).to.be.below(5000000)
105
106 const path = join(root(), 'test1', 'videos', video.uuid + '-' + file.resolution.id + '.mp4')
107 const bitrate = await getVideoFileBitrate(path)
108 const fps = await getVideoFileFPS(path)
109 const resolution = await getVideoFileResolution(path)
110
111 expect(resolution.videoFileResolution).to.equal(file.resolution.id)
112 expect(bitrate).to.be.below(getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
113 }
114 }
115 })
116
117 after(async function () {
118 killallServers(servers)
119 })
120})
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts
new file mode 100644
index 000000000..a6d829a9f
--- /dev/null
+++ b/server/tests/helpers/core-utils.ts
@@ -0,0 +1,48 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 parseBytes
7} from '../../helpers/core-utils'
8
9const expect = chai.expect
10
11describe('Parse Bytes', function () {
12 it('Should pass when given valid value', async function () {
13 // just return it
14 expect(parseBytes(1024)).to.be.eq(1024)
15 expect(parseBytes(1048576)).to.be.eq(1048576)
16 expect(parseBytes('1024')).to.be.eq(1024)
17 expect(parseBytes('1048576')).to.be.eq(1048576)
18
19 // sizes
20 expect(parseBytes('1B')).to.be.eq(1024)
21 expect(parseBytes('1MB')).to.be.eq(1048576)
22 expect(parseBytes('1GB')).to.be.eq(1073741824)
23 expect(parseBytes('1TB')).to.be.eq(1099511627776)
24
25 expect(parseBytes('5GB')).to.be.eq(5368709120)
26 expect(parseBytes('5TB')).to.be.eq(5497558138880)
27
28 expect(parseBytes('1024B')).to.be.eq(1048576)
29 expect(parseBytes('1024MB')).to.be.eq(1073741824)
30 expect(parseBytes('1024GB')).to.be.eq(1099511627776)
31 expect(parseBytes('1024TB')).to.be.eq(1125899906842624)
32
33 // with whitespace
34 expect(parseBytes('1 GB')).to.be.eq(1073741824)
35 expect(parseBytes('1\tGB')).to.be.eq(1073741824)
36
37 // sum value
38 expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600)
39 expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120)
40 expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880)
41 expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880)
42 expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224)
43 })
44
45 it('Should be invalid when given invalid value', async function () {
46 expect(parseBytes('6GB 1GB')).to.be.eq(6)
47 })
48})
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts
new file mode 100644
index 000000000..40c7dc70e
--- /dev/null
+++ b/server/tests/helpers/index.ts
@@ -0,0 +1 @@
import './core-utils'
diff --git a/server/tests/utils/miscs/miscs.ts b/server/tests/utils/miscs/miscs.ts
index b2f80e9b1..589daa420 100644
--- a/server/tests/utils/miscs/miscs.ts
+++ b/server/tests/utils/miscs/miscs.ts
@@ -4,7 +4,8 @@ import * as chai from 'chai'
4import { isAbsolute, join } from 'path' 4import { isAbsolute, join } from 'path'
5import * as request from 'supertest' 5import * as request from 'supertest'
6import * as WebTorrent from 'webtorrent' 6import * as WebTorrent from 'webtorrent'
7import { readFile } from 'fs-extra' 7import { pathExists, readFile } from 'fs-extra'
8import * as ffmpeg from 'fluent-ffmpeg'
8 9
9const expect = chai.expect 10const expect = chai.expect
10let webtorrent = new WebTorrent() 11let webtorrent = new WebTorrent()
@@ -51,14 +52,41 @@ async function testImage (url: string, imageName: string, imagePath: string, ext
51 expect(data.length).to.be.below(maxLength) 52 expect(data.length).to.be.below(maxLength)
52} 53}
53 54
54function buildAbsoluteFixturePath (path: string) { 55function buildAbsoluteFixturePath (path: string, customTravisPath = false) {
55 if (isAbsolute(path)) { 56 if (isAbsolute(path)) {
56 return path 57 return path
57 } 58 }
58 59
60 if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path)
61
59 return join(__dirname, '..', '..', 'fixtures', path) 62 return join(__dirname, '..', '..', 'fixtures', path)
60} 63}
61 64
65async function generateHighBitrateVideo () {
66 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
67
68 const exists = await pathExists(tempFixturePath)
69 if (!exists) {
70
71 // Generate a random, high bitrate video on the fly, so we don't have to include
72 // a large file in the repo. The video needs to have a certain minimum length so
73 // that FFmpeg properly applies bitrate limits.
74 // https://stackoverflow.com/a/15795112
75 return new Promise<string>(async (res, rej) => {
76 ffmpeg()
77 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
78 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
79 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
80 .output(tempFixturePath)
81 .on('error', rej)
82 .on('end', () => res(tempFixturePath))
83 .run()
84 })
85 }
86
87 return tempFixturePath
88}
89
62// --------------------------------------------------------------------------- 90// ---------------------------------------------------------------------------
63 91
64export { 92export {
@@ -68,5 +96,6 @@ export {
68 immutableAssign, 96 immutableAssign,
69 testImage, 97 testImage,
70 buildAbsoluteFixturePath, 98 buildAbsoluteFixturePath,
71 root 99 root,
100 generateHighBitrateVideo
72} 101}
diff --git a/server/tests/utils/requests/requests.ts b/server/tests/utils/requests/requests.ts
index 27a529eda..5796540f7 100644
--- a/server/tests/utils/requests/requests.ts
+++ b/server/tests/utils/requests/requests.ts
@@ -37,9 +37,7 @@ function makeDeleteRequest (options: {
37 37
38 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 38 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
39 39
40 return req 40 return req.expect(options.statusCodeExpected)
41 .expect('Content-Type', /json/)
42 .expect(options.statusCodeExpected)
43} 41}
44 42
45function makeUploadRequest (options: { 43function makeUploadRequest (options: {
diff --git a/server/tests/utils/server/follows.ts b/server/tests/utils/server/follows.ts
index 8a65a958b..7741757a6 100644
--- a/server/tests/utils/server/follows.ts
+++ b/server/tests/utils/server/follows.ts
@@ -2,7 +2,7 @@ import * as request from 'supertest'
2import { ServerInfo } from './servers' 2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 3import { waitJobs } from './jobs'
4 4
5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string) { 5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
6 const path = '/api/v1/server/followers' 6 const path = '/api/v1/server/followers'
7 7
8 return request(url) 8 return request(url)
@@ -10,12 +10,13 @@ function getFollowersListPaginationAndSort (url: string, start: number, count: n
10 .query({ start }) 10 .query({ start })
11 .query({ count }) 11 .query({ count })
12 .query({ sort }) 12 .query({ sort })
13 .query({ search })
13 .set('Accept', 'application/json') 14 .set('Accept', 'application/json')
14 .expect(200) 15 .expect(200)
15 .expect('Content-Type', /json/) 16 .expect('Content-Type', /json/)
16} 17}
17 18
18function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string) { 19function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
19 const path = '/api/v1/server/following' 20 const path = '/api/v1/server/following'
20 21
21 return request(url) 22 return request(url)
@@ -23,6 +24,7 @@ function getFollowingListPaginationAndSort (url: string, start: number, count: n
23 .query({ start }) 24 .query({ start })
24 .query({ count }) 25 .query({ count })
25 .query({ sort }) 26 .query({ sort })
27 .query({ search })
26 .set('Accept', 'application/json') 28 .set('Accept', 'application/json')
27 .expect(200) 29 .expect(200)
28 .expect('Content-Type', /json/) 30 .expect('Content-Type', /json/)
diff --git a/server/tests/utils/users/blocklist.ts b/server/tests/utils/users/blocklist.ts
new file mode 100644
index 000000000..35b537571
--- /dev/null
+++ b/server/tests/utils/users/blocklist.ts
@@ -0,0 +1,198 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeDeleteRequest, makePostBodyRequest } from '../index'
4import { makeGetRequest } from '../requests/requests'
5
6function 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
25function 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
39function 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
50function 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
69function 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
83function 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
94function 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
113function 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
127function 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
138function 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
157function 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
171function 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
184export {
185 getAccountBlocklistByAccount,
186 addAccountToAccountBlocklist,
187 removeAccountFromAccountBlocklist,
188 getServerBlocklistByAccount,
189 addServerToAccountBlocklist,
190 removeServerFromAccountBlocklist,
191
192 getAccountBlocklistByServer,
193 addAccountToServerBlocklist,
194 removeAccountFromServerBlocklist,
195 getServerBlocklistByServer,
196 addServerToServerBlocklist,
197 removeServerFromServerBlocklist
198}
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index 41d8ce265..d77233d62 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -112,7 +112,7 @@ function getUsersList (url: string, accessToken: string) {
112 .expect('Content-Type', /json/) 112 .expect('Content-Type', /json/)
113} 113}
114 114
115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) { 115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) {
116 const path = '/api/v1/users' 116 const path = '/api/v1/users'
117 117
118 return request(url) 118 return request(url)
@@ -120,6 +120,7 @@ function getUsersListPaginationAndSort (url: string, accessToken: string, start:
120 .query({ start }) 120 .query({ start })
121 .query({ count }) 121 .query({ count })
122 .query({ sort }) 122 .query({ sort })
123 .query({ search })
123 .set('Accept', 'application/json') 124 .set('Accept', 'application/json')
124 .set('Authorization', 'Bearer ' + accessToken) 125 .set('Authorization', 'Bearer ' + accessToken)
125 .expect(200) 126 .expect(200)
diff --git a/server/tests/utils/videos/video-comments.ts b/server/tests/utils/videos/video-comments.ts
index 1b9ee452e..7d4cae364 100644
--- a/server/tests/utils/videos/video-comments.ts
+++ b/server/tests/utils/videos/video-comments.ts
@@ -1,7 +1,7 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { makeDeleteRequest } from '../' 2import { makeDeleteRequest } from '../'
3 3
4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { 4function 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
19function getVideoThreadComments (url: string, videoId: number | string, threadId: number) { 20function 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
29function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) { 33function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
diff --git a/server/tests/utils/videos/video-history.ts b/server/tests/utils/videos/video-history.ts
new file mode 100644
index 000000000..7635478f7
--- /dev/null
+++ b/server/tests/utils/videos/video-history.ts
@@ -0,0 +1,14 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 })
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 userWatchVideo
14}
diff --git a/server/tools/README.md b/server/tools/README.md
new file mode 100644
index 000000000..6b94d74e5
--- /dev/null
+++ b/server/tools/README.md
@@ -0,0 +1,82 @@
1peertube(8) -- companion CLI for PeerTube
2=========================================
3
4SYNOPSIS
5--------
6
7```
8peertube [command] [options]
9```
10
11DESCRIPTION
12-----------
13
14`peertube` wraps various utilities around PeerTube that are used either on a running local, running remote, or cold local instance.
15
16COMMANDS
17--------
18
19Unless otherwise specified, every command can be queried for its own help or manual by passing its name to the `help` command, or by using the `--help` option.
20
21`auth [action]`: stores credentials for your accounts on remote instances so that you don't need to pass them at every command
22
23`upload|up`: upload a video to a remote instance
24
25 $ peertube upload \
26 -u "PEERTUBE_URL" \
27 -U "PEERTUBE_USER" \
28 --password "PEERTUBE_PASSWORD"
29
30`import-videos|import`: import a video from a streaming platform to a remote instance
31
32 $ peertube import \
33 -u "PEERTUBE_URL" \
34 -U "PEERTUBE_USER" \
35 --password "PEERTUBE_PASSWORD" \
36 -t "TARGET_URL"
37
38 The target URL can be directly the video file, or any of the supported sites of youtube-dl. The video is downloaded locally and then uploaded. Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection…
39
40`watch|w`: watch a video in the terminal ✩°。⋆
41
42 -g, --gui <player> player type (default: ascii)
43 -i, --invert invert colors (ascii player only)
44 -r, --resolution <res> video resolution (default: 720)
45
46 It provides support for different players:
47
48 - ascii (default ; plays in ascii art in your terminal!)
49 - mpv
50 - mplayer
51 - vlc
52 - stdout
53 - xbmc
54 - airplay
55 - chromecast
56
57`repl`: interact with the application libraries and objects even when PeerTube is not running
58
59 Type .help to see the repl-only functions, or to see the available PeerTube core functions:
60
61 repl> lodash.keys(context)
62
63`help [cmd]`: display help for [cmd]
64
65EXAMPLES
66--------
67
68 $ peertube auth add -u "PEERTUBE_URL" -U "PEERTUBE_USER" --password "PEERTUBE_PASSWORD"
69 $ peertube up <videoFile>
70 $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10
71
72SEE ALSO
73--------
74
75[PeerTube Tools Documentation](https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/tools.md)
76
77[PeerTube Admin Documentation](https://docs.joinpeertube.org/lang/en/docs/)
78
79REPORTING BUGS
80--------------
81
82See [PeerTube repository](https://github.com/Chocobozzz/PeerTube).
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 53b20964e..108c44218 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -23,7 +23,7 @@ async function getSettings () {
23 if (err) { 23 if (err) {
24 return rej(err) 24 return rej(err)
25 } 25 }
26 return res(data || settings) 26 return res(Object.keys(data).length === 0 ? settings : data)
27 }) 27 })
28 }) 28 })
29} 29}
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 13090a028..21505b79d 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -10,6 +10,7 @@ import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import * as prompt from 'prompt' 11import * as prompt from 'prompt'
12import { remove } from 'fs-extra' 12import { remove } from 'fs-extra'
13import { sha256 } from '../helpers/core-utils'
13import { safeGetYoutubeDL } from '../helpers/youtube-dl' 14import { safeGetYoutubeDL } from '../helpers/youtube-dl'
14import { getSettings, netrc } from './cli' 15import { getSettings, netrc } from './cli'
15 16
@@ -133,8 +134,7 @@ async function run (user, url: string) {
133 await processVideo(info, program['language'], processOptions.cwd, url, user) 134 await processVideo(info, program['language'], processOptions.cwd, url, user)
134 } 135 }
135 136
136 // https://www.youtube.com/watch?v=2Upx39TBc1s 137 console.log('Video/s for user %s imported: %s', program['username'], program['targetUrl'])
137 console.log('I\'m finished!')
138 process.exit(0) 138 process.exit(0)
139 }) 139 })
140} 140}
@@ -155,7 +155,7 @@ function processVideo (info: any, languageCode: string, cwd: string, url: string
155 return res() 155 return res()
156 } 156 }
157 157
158 const path = join(cwd, new Date().getTime() + '.mp4') 158 const path = join(cwd, sha256(videoInfo.url) + '.mp4')
159 159
160 console.log('Downloading video "%s"...', videoInfo.title) 160 console.log('Downloading video "%s"...', videoInfo.title)
161 161
@@ -192,7 +192,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
192 192
193 let thumbnailfile 193 let thumbnailfile
194 if (videoInfo.thumbnail) { 194 if (videoInfo.thumbnail) {
195 thumbnailfile = join(cwd, 'thumbnail.jpg') 195 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg')
196 196
197 await doRequestAndSaveToFile({ 197 await doRequestAndSaveToFile({
198 method: 'GET', 198 method: 'GET',
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts
new file mode 100644
index 000000000..6800ff8ab
--- /dev/null
+++ b/server/tools/peertube-repl.ts
@@ -0,0 +1,79 @@
1import * as repl from 'repl'
2import * as path from 'path'
3import * as _ from 'lodash'
4import * as uuidv1 from 'uuid/v1'
5import * as uuidv3 from 'uuid/v3'
6import * as uuidv4 from 'uuid/v4'
7import * as uuidv5 from 'uuid/v5'
8import * as Sequelize from 'sequelize'
9import * as YoutubeDL from 'youtube-dl'
10
11import { initDatabaseModels, sequelizeTypescript } from '../initializers'
12import * as cli from '../tools/cli'
13import { logger } from '../helpers/logger'
14import * as constants from '../initializers/constants'
15import * as modelsUtils from '../models/utils'
16import * as coreUtils from '../helpers/core-utils'
17import * as ffmpegUtils from '../helpers/ffmpeg-utils'
18import * as peertubeCryptoUtils from '../helpers/peertube-crypto'
19import * as signupUtils from '../helpers/signup'
20import * as utils from '../helpers/utils'
21import * as YoutubeDLUtils from '../helpers/youtube-dl'
22
23let versionCommitHash
24
25const 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
75start().then((data) => {
76 // do nothing
77}).catch((err) => {
78 console.error(err)
79})
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
index ad76bafb4..c8b9fa744 100755
--- a/server/tools/peertube.ts
+++ b/server/tools/peertube.ts
@@ -17,6 +17,7 @@ program
17 .command('import-videos', 'import a video from a streaming platform').alias('import') 17 .command('import-videos', 'import a video from a streaming platform').alias('import')
18 .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') 18 .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token')
19 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') 19 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
20 .command('repl', 'initiate a REPL to access internals')
20 21
21/* Not Yet Implemented */ 22/* Not Yet Implemented */
22program 23program