aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/accounts.ts5
-rw-r--r--server/controllers/api/search.ts2
-rw-r--r--server/controllers/api/users/index.ts2
-rw-r--r--server/controllers/api/users/me.ts3
-rw-r--r--server/controllers/api/users/my-blocklist.ts125
-rw-r--r--server/controllers/api/video-channel.ts2
-rw-r--r--server/controllers/api/videos/comment.ts12
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/utils.ts5
-rw-r--r--server/initializers/constants.ts5
-rw-r--r--server/initializers/database.ts6
-rw-r--r--server/lib/blocklist.ts40
-rw-r--r--server/lib/video-comment.ts6
-rw-r--r--server/middlewares/validators/blocklist.ts94
-rw-r--r--server/middlewares/validators/index.ts2
-rw-r--r--server/middlewares/validators/server.ts33
-rw-r--r--server/middlewares/validators/sort.ts8
-rw-r--r--server/models/account/account-blocklist.ts111
-rw-r--r--server/models/server/server-blocklist.ts121
-rw-r--r--server/models/server/server.ts6
-rw-r--r--server/models/utils.ts18
-rw-r--r--server/models/video/video-comment.ts95
-rw-r--r--server/models/video/video.ts40
-rw-r--r--server/tests/api/check-params/blocklist.ts222
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/users/account-blocklist.ts294
-rw-r--r--server/tests/utils/requests/requests.ts4
-rw-r--r--server/tests/utils/users/blocklist.ts103
-rw-r--r--server/tests/utils/videos/video-comments.ts14
-rw-r--r--shared/models/blocklist/account-block.model.ts7
-rw-r--r--shared/models/blocklist/index.ts2
-rw-r--r--shared/models/blocklist/server-block.model.ts9
-rw-r--r--shared/models/index.ts1
33 files changed, 1344 insertions, 56 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8e3f60010..86ef2aed1 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,7 +1,8 @@
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,
@@ -90,7 +91,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
90 nsfw: buildNSFWFilter(res, req.query.nsfw), 91 nsfw: buildNSFWFilter(res, req.query.nsfw),
91 withFiles: false, 92 withFiles: false,
92 accountId: account.id, 93 accountId: account.id,
93 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 94 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
94 }) 95 })
95 96
96 return res.json(getFormattedObjects(resultList.data, resultList.total)) 97 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index a8a6cfb08..534305ba6 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -119,7 +119,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw), 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 filter: query.filter, 121 filter: query.filter,
122 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 122 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
123 }) 123 })
124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
125 125
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 4f8137c03..9fcb8077f 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user'
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',
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 591ec6b25..ebe668110 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -238,7 +238,8 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
238 nsfw: buildNSFWFilter(res, req.query.nsfw), 238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter, 239 filter: req.query.filter as VideoFilter,
240 withFiles: false, 240 withFiles: false,
241 actorId: user.Account.Actor.id 241 actorId: user.Account.Actor.id,
242 user
242 }) 243 })
243 244
244 return res.json(getFormattedObjects(resultList.data, resultList.total)) 245 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
new file mode 100644
index 000000000..e955ffde9
--- /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 serverGetValidator,
10 setDefaultPagination,
11 setDefaultSort,
12 unblockAccountByAccountValidator
13} from '../../../middlewares'
14import {
15 accountsBlocklistSortValidator,
16 blockAccountByAccountValidator,
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(blockAccountByAccountValidator),
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(serverGetValidator),
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 c84d1be58..9bf3c5fd8 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -219,7 +219,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
219 nsfw: buildNSFWFilter(res, req.query.nsfw), 219 nsfw: buildNSFWFilter(res, req.query.nsfw),
220 withFiles: false, 220 withFiles: false,
221 videoChannelId: videoChannelInstance.id, 221 videoChannelId: videoChannelInstance.id,
222 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 222 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
223 }) 223 })
224 224
225 return res.json(getFormattedObjects(resultList.data, resultList.total)) 225 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 4f2b4faee..3875c8f79 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -8,7 +8,7 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
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
@@ -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 6a73e13d0..664154406 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -437,7 +437,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
437 nsfw: buildNSFWFilter(res, req.query.nsfw), 437 nsfw: buildNSFWFilter(res, req.query.nsfw),
438 filter: req.query.filter as VideoFilter, 438 filter: req.query.filter as VideoFilter,
439 withFiles: false, 439 withFiles: false,
440 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 440 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
441 }) 441 })
442 442
443 return res.json(getFormattedObjects(resultList.data, resultList.total)) 443 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 39afb4e7b..049c3f8bc 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () {
40 const application = await ApplicationModel.load() 40 const application = await ApplicationModel.load()
41 if (!application) throw Error('Could not load Application from database.') 41 if (!application) throw Error('Could not load Application from database.')
42 42
43 return application.Account.Actor 43 const actor = application.Account.Actor
44 actor.Account = application.Account
45
46 return actor
44}) 47})
45 48
46function generateVideoTmpPath (target: string | ParseTorrent) { 49function generateVideoTmpPath (target: string | ParseTorrent) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 49ee13c10..cf00da2c7 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -47,7 +47,10 @@ const SORTABLE_COLUMNS = {
47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], 47 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ],
48 48
49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], 49 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ],
50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] 50 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
51
52 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
53 SERVERS_BLOCKLIST: [ 'createdAt' ]
51} 54}
52 55
53const OAUTH_LIFETIME = { 56const OAUTH_LIFETIME = {
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 482c03b31..dd5b9bf67 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -29,6 +29,8 @@ import { VideoViewModel } from '../models/video/video-views'
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' 31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist'
32 34
33require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 35require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
34 36
@@ -91,7 +93,9 @@ async function initDatabaseModels (silent: boolean) {
91 VideoImportModel, 93 VideoImportModel,
92 VideoViewModel, 94 VideoViewModel,
93 VideoRedundancyModel, 95 VideoRedundancyModel,
94 UserVideoHistoryModel 96 UserVideoHistoryModel,
97 AccountBlocklistModel,
98 ServerBlocklistModel
95 ]) 99 ])
96 100
97 // Check extensions exist in the database 101 // Check extensions exist in the database
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
new file mode 100644
index 000000000..394c24537
--- /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.create({
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.create({
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/video-comment.ts b/server/lib/video-comment.ts
index 70ba7c303..59bce7520 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
64 } 64 }
65 65
66 const parentCommentThread = idx[childComment.inReplyToCommentId] 66 const parentCommentThread = idx[childComment.inReplyToCommentId]
67 if (!parentCommentThread) { 67 // Maybe the parent comment was blocked by the admin/user
68 const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}` 68 if (!parentCommentThread) continue
69 throw new Error(msg)
70 }
71 69
72 parentCommentThread.children.push(childCommentThread) 70 parentCommentThread.children.push(childCommentThread)
73 idx[childComment.id] = childCommentThread 71 idx[childComment.id] = childCommentThread
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
new file mode 100644
index 000000000..9dbd5e512
--- /dev/null
+++ b/server/middlewares/validators/blocklist.ts
@@ -0,0 +1,94 @@
1import { param, body } 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'
10
11const blockAccountByAccountValidator = [
12 body('accountName').exists().withMessage('Should have an account name with host'),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body })
16
17 if (areValidationErrors(req, res)) return
18 if (!await isAccountNameWithHostExist(req.body.accountName, res)) return
19
20 return next()
21 }
22]
23
24const unblockAccountByAccountValidator = [
25 param('accountName').exists().withMessage('Should have an account name with host'),
26
27 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params })
29
30 if (areValidationErrors(req, res)) return
31 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
32
33 const user = res.locals.oauth.token.User as UserModel
34 const targetAccount = res.locals.account
35 if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return
36
37 return next()
38 }
39]
40
41const unblockServerByAccountValidator = [
42 param('host').custom(isHostValid).withMessage('Should have an account name with host'),
43
44 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
45 logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params })
46
47 if (areValidationErrors(req, res)) return
48
49 const user = res.locals.oauth.token.User as UserModel
50 if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return
51
52 return next()
53 }
54]
55
56// ---------------------------------------------------------------------------
57
58export {
59 blockAccountByAccountValidator,
60 unblockAccountByAccountValidator,
61 unblockServerByAccountValidator
62}
63
64// ---------------------------------------------------------------------------
65
66async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) {
67 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
68 if (!accountBlock) {
69 res.status(404)
70 .send({ error: 'Account block entry not found.' })
71 .end()
72
73 return false
74 }
75
76 res.locals.accountBlock = accountBlock
77
78 return true
79}
80
81async function isUnblockServerExists (accountId: number, host: string, res: express.Response) {
82 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
83 if (!serverBlock) {
84 res.status(404)
85 .send({ error: 'Server block entry not found.' })
86 .end()
87
88 return false
89 }
90
91 res.locals.serverBlock = serverBlock
92
93 return true
94}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 17226614c..46c7f0f3a 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,4 +1,5 @@
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'
@@ -10,3 +11,4 @@ export * from './user-subscriptions'
10export * from './videos' 11export * from './videos'
11export * from './webfinger' 12export * from './webfinger'
12export * from './search' 13export * from './search'
14export * from './server'
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
new file mode 100644
index 000000000..a491dfeb3
--- /dev/null
+++ b/server/middlewares/validators/server.ts
@@ -0,0 +1,33 @@
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/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
new file mode 100644
index 000000000..bacd122e8
--- /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: 'AccountBlocked'
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: 'AccountBlocked',
71 onDelete: 'CASCADE'
72 })
73 AccountBlocked: 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 accountBlocked: this.AccountBlocked.toFormattedJSON(),
108 createdAt: this.createdAt
109 }
110 }
111}
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
new file mode 100644
index 000000000..705ed2c6b
--- /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 ServerBlocked: 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 serverBlocked: this.ServerBlocked.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..50c865e75 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -64,9 +64,27 @@ 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 "accountId" FROM "accountBlocklist" WHERE "targetAccountId" = user.account.id
76 // UNION ALL
77 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
78 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
79 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
80
81 return query
82}
83
67// --------------------------------------------------------------------------- 84// ---------------------------------------------------------------------------
68 85
69export { 86export {
87 buildBlockedAccountSQL,
70 SortType, 88 SortType,
71 getSort, 89 getSort,
72 getVideoSort, 90 getVideoSort,
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index f84c1880c..08c6b3ff0 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.Account.id
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.Account.id
334
290 const query = { 335 const query = {
291 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
292 where: { 337 where: {
@@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
294 [ Sequelize.Op.or ]: [ 339 [ Sequelize.Op.or ]: [
295 { id: threadId }, 340 { id: threadId },
296 { originCommentId: threadId } 341 { originCommentId: threadId }
297 ] 342 ],
343 accountId: {
344 [Sequelize.Op.notIn]: Sequelize.literal(
345 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
346 )
347 }
298 } 348 }
299 } 349 }
300 350
351 const scopes: any[] = [
352 ScopeNames.WITH_ACCOUNT,
353 {
354 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
355 }
356 ]
357
301 return VideoCommentModel 358 return VideoCommentModel
302 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) 359 .scope(scopes)
303 .findAndCountAll(query) 360 .findAndCountAll(query)
304 .then(({ rows, count }) => { 361 .then(({ rows, count }) => {
305 return { total: count, data: rows } 362 return { total: count, data: rows }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 4f3f75613..eab99cba7 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'
@@ -93,6 +93,7 @@ import {
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' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user'
96 97
97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
98const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -138,6 +139,7 @@ type ForAPIOptions = {
138} 139}
139 140
140type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number
141 actorId: number 143 actorId: number
142 includeLocalVideos: boolean 144 includeLocalVideos: boolean
143 filter?: VideoFilter 145 filter?: VideoFilter
@@ -151,6 +153,7 @@ type AvailableForListIDsOptions = {
151 accountId?: number 153 accountId?: number
152 videoChannelId?: number 154 videoChannelId?: number
153 trendingDays?: number 155 trendingDays?: number
156 user?: UserModel
154} 157}
155 158
156@Scopes({ 159@Scopes({
@@ -235,6 +238,15 @@ type AvailableForListIDsOptions = {
235 ) 238 )
236 } 239 }
237 ] 240 ]
241 },
242 channelId: {
243 [ Sequelize.Op.notIn ]: Sequelize.literal(
244 '(' +
245 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
246 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
247 ')' +
248 ')'
249 )
238 } 250 }
239 }, 251 },
240 include: [] 252 include: []
@@ -975,10 +987,10 @@ export class VideoModel extends Model<VideoModel> {
975 videoChannelId?: number, 987 videoChannelId?: number,
976 actorId?: number 988 actorId?: number
977 trendingDays?: number, 989 trendingDays?: number,
978 userId?: number 990 user?: UserModel
979 }, countVideos = true) { 991 }, countVideos = true) {
980 if (options.filter && options.filter === 'all-local' && !options.userId) { 992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
981 throw new Error('Try to filter all-local but no userId is provided') 993 throw new Error('Try to filter all-local but no user has not the see all videos right')
982 } 994 }
983 995
984 const query: IFindOptions<VideoModel> = { 996 const query: IFindOptions<VideoModel> = {
@@ -994,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
994 query.group = 'VideoModel.id' 1006 query.group = 'VideoModel.id'
995 } 1007 }
996 1008
1009 const serverActor = await getServerActor()
1010
997 // actorId === null has a meaning, so just check undefined 1011 // actorId === null has a meaning, so just check undefined
998 const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id 1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
999 1013
1000 const queryOptions = { 1014 const queryOptions = {
1001 actorId, 1015 actorId,
1016 serverAccountId: serverActor.Account.id,
1002 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
1003 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
1004 licenceOneOf: options.licenceOneOf, 1019 licenceOneOf: options.licenceOneOf,
@@ -1010,7 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
1010 accountId: options.accountId, 1025 accountId: options.accountId,
1011 videoChannelId: options.videoChannelId, 1026 videoChannelId: options.videoChannelId,
1012 includeLocalVideos: options.includeLocalVideos, 1027 includeLocalVideos: options.includeLocalVideos,
1013 userId: options.userId, 1028 user: options.user,
1014 trendingDays 1029 trendingDays
1015 } 1030 }
1016 1031
@@ -1033,7 +1048,7 @@ export class VideoModel extends Model<VideoModel> {
1033 tagsAllOf?: string[] 1048 tagsAllOf?: string[]
1034 durationMin?: number // seconds 1049 durationMin?: number // seconds
1035 durationMax?: number // seconds 1050 durationMax?: number // seconds
1036 userId?: number, 1051 user?: UserModel,
1037 filter?: VideoFilter 1052 filter?: VideoFilter
1038 }) { 1053 }) {
1039 const whereAnd = [] 1054 const whereAnd = []
@@ -1104,6 +1119,7 @@ export class VideoModel extends Model<VideoModel> {
1104 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1105 const queryOptions = { 1120 const queryOptions = {
1106 actorId: serverActor.id, 1121 actorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id,
1107 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1108 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
1109 categoryOneOf: options.categoryOneOf, 1125 categoryOneOf: options.categoryOneOf,
@@ -1111,7 +1127,7 @@ export class VideoModel extends Model<VideoModel> {
1111 languageOneOf: options.languageOneOf, 1127 languageOneOf: options.languageOneOf,
1112 tagsOneOf: options.tagsOneOf, 1128 tagsOneOf: options.tagsOneOf,
1113 tagsAllOf: options.tagsAllOf, 1129 tagsAllOf: options.tagsAllOf,
1114 userId: options.userId, 1130 user: options.user,
1115 filter: options.filter 1131 filter: options.filter
1116 } 1132 }
1117 1133
@@ -1287,7 +1303,7 @@ export class VideoModel extends Model<VideoModel> {
1287 1303
1288 private static async getAvailableForApi ( 1304 private static async getAvailableForApi (
1289 query: IFindOptions<VideoModel>, 1305 query: IFindOptions<VideoModel>,
1290 options: AvailableForListIDsOptions & { userId?: number}, 1306 options: AvailableForListIDsOptions,
1291 countVideos = true 1307 countVideos = true
1292 ) { 1308 ) {
1293 const idsScope = { 1309 const idsScope = {
@@ -1320,8 +1336,8 @@ export class VideoModel extends Model<VideoModel> {
1320 } 1336 }
1321 ] 1337 ]
1322 1338
1323 if (options.userId) { 1339 if (options.user) {
1324 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] }) 1340 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1325 } 1341 }
1326 1342
1327 const secondQuery = { 1343 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..8117c46a6
--- /dev/null
+++ b/server/tests/api/check-params/blocklist.ts
@@ -0,0 +1,222 @@
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
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
23 before(async function () {
24 this.timeout(60000)
25
26 await flushTests()
27
28 servers = await flushAndRunMultipleServers(2)
29 await setAccessTokensToServers(servers)
30
31 server = servers[0]
32
33 const user = { username: 'user1', password: 'password' }
34 await createUser(server.url, server.accessToken, user.username, user.password)
35
36 await doubleFollow(servers[0], servers[1])
37 })
38
39 // ---------------------------------------------------------------
40
41 describe('When managing user blocklist', function () {
42 const path = '/api/v1/users/me/blocklist/accounts'
43
44 describe('When managing user accounts blocklist', function () {
45
46 describe('When listing blocked accounts', function () {
47 it('Should fail with an unauthenticated user', async function () {
48 await makeGetRequest({
49 url: server.url,
50 path,
51 statusCodeExpected: 401
52 })
53 })
54
55 it('Should fail with a bad start pagination', async function () {
56 await checkBadStartPagination(server.url, path, server.accessToken)
57 })
58
59 it('Should fail with a bad count pagination', async function () {
60 await checkBadCountPagination(server.url, path, server.accessToken)
61 })
62
63 it('Should fail with an incorrect sort', async function () {
64 await checkBadSortPagination(server.url, path, server.accessToken)
65 })
66 })
67
68 describe('When blocking an account', function () {
69 it('Should fail with an unauthenticated user', async function () {
70 await makePostBodyRequest({
71 url: server.url,
72 path,
73 fields: { accountName: 'user1' },
74 statusCodeExpected: 401
75 })
76 })
77
78 it('Should fail with an unknown account', async function () {
79 await makePostBodyRequest({
80 url: server.url,
81 token: server.accessToken,
82 path,
83 fields: { accountName: 'user2' },
84 statusCodeExpected: 404
85 })
86 })
87
88 it('Should succeed with the correct params', async function () {
89 await makePostBodyRequest({
90 url: server.url,
91 token: server.accessToken,
92 path,
93 fields: { accountName: 'user1' },
94 statusCodeExpected: 204
95 })
96 })
97 })
98
99 describe('When unblocking an account', function () {
100 it('Should fail with an unauthenticated user', async function () {
101 await makeDeleteRequest({
102 url: server.url,
103 path: path + '/user1',
104 statusCodeExpected: 401
105 })
106 })
107
108 it('Should fail with an unknown account block', async function () {
109 await makeDeleteRequest({
110 url: server.url,
111 path: path + '/user2',
112 token: server.accessToken,
113 statusCodeExpected: 404
114 })
115 })
116
117 it('Should succeed with the correct params', async function () {
118 await makeDeleteRequest({
119 url: server.url,
120 path: path + '/user1',
121 token: server.accessToken,
122 statusCodeExpected: 204
123 })
124 })
125 })
126 })
127
128 describe('When managing user servers blocklist', function () {
129 const path = '/api/v1/users/me/blocklist/servers'
130
131 describe('When listing blocked servers', function () {
132 it('Should fail with an unauthenticated user', async function () {
133 await makeGetRequest({
134 url: server.url,
135 path,
136 statusCodeExpected: 401
137 })
138 })
139
140 it('Should fail with a bad start pagination', async function () {
141 await checkBadStartPagination(server.url, path, server.accessToken)
142 })
143
144 it('Should fail with a bad count pagination', async function () {
145 await checkBadCountPagination(server.url, path, server.accessToken)
146 })
147
148 it('Should fail with an incorrect sort', async function () {
149 await checkBadSortPagination(server.url, path, server.accessToken)
150 })
151 })
152
153 describe('When blocking a server', function () {
154 it('Should fail with an unauthenticated user', async function () {
155 await makePostBodyRequest({
156 url: server.url,
157 path,
158 fields: { host: 'localhost:9002' },
159 statusCodeExpected: 401
160 })
161 })
162
163 it('Should fail with an unknown server', async function () {
164 await makePostBodyRequest({
165 url: server.url,
166 token: server.accessToken,
167 path,
168 fields: { host: 'localhost:9003' },
169 statusCodeExpected: 404
170 })
171 })
172
173 it('Should succeed with the correct params', async function () {
174 await makePostBodyRequest({
175 url: server.url,
176 token: server.accessToken,
177 path,
178 fields: { host: 'localhost:9002' },
179 statusCodeExpected: 204
180 })
181 })
182 })
183
184 describe('When unblocking a server', function () {
185 it('Should fail with an unauthenticated user', async function () {
186 await makeDeleteRequest({
187 url: server.url,
188 path: path + '/localhost:9002',
189 statusCodeExpected: 401
190 })
191 })
192
193 it('Should fail with an unknown server block', async function () {
194 await makeDeleteRequest({
195 url: server.url,
196 path: path + '/localhost:9003',
197 token: server.accessToken,
198 statusCodeExpected: 404
199 })
200 })
201
202 it('Should succeed with the correct params', async function () {
203 await makeDeleteRequest({
204 url: server.url,
205 path: path + '/localhost:9002',
206 token: server.accessToken,
207 statusCodeExpected: 204
208 })
209 })
210 })
211 })
212 })
213
214 after(async function () {
215 killallServers(servers)
216
217 // Keep the logs if the test failed
218 if (this['ok']) {
219 await flushTests()
220 }
221 })
222})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index bfc550ae5..877ceb0a7 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,5 +1,6 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './accounts' 2import './accounts'
3import './blocklist'
3import './config' 4import './config'
4import './follows' 5import './follows'
5import './jobs' 6import './jobs'
diff --git a/server/tests/api/users/account-blocklist.ts b/server/tests/api/users/account-blocklist.ts
new file mode 100644
index 000000000..00ad51461
--- /dev/null
+++ b/server/tests/api/users/account-blocklist.ts
@@ -0,0 +1,294 @@
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 } 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 addServerToAccountBlocklist,
29 getAccountBlocklistByAccount, getServerBlocklistByAccount,
30 removeAccountFromAccountBlocklist,
31 removeServerFromAccountBlocklist
32} from '../../utils/users/blocklist'
33
34const expect = chai.expect
35
36async function checkAllVideos (url: string, token: string) {
37 const res = await getVideosListWithToken(url, token)
38
39 expect(res.body.data).to.have.lengthOf(4)
40}
41
42async function checkAllComments (url: string, token: string, videoUUID: string) {
43 const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 5, '-createdAt', token)
44
45 const threads: VideoComment[] = resThreads.body.data
46 expect(threads).to.have.lengthOf(2)
47
48 for (const thread of threads) {
49 const res = await getVideoThreadComments(url, videoUUID, thread.id, token)
50
51 const tree: VideoCommentThreadTree = res.body
52 expect(tree.children).to.have.lengthOf(1)
53 }
54}
55
56describe('Test accounts blocklist', function () {
57 let servers: ServerInfo[]
58 let videoUUID1: string
59 let videoUUID2: string
60 let userToken1: string
61 let userToken2: string
62
63 before(async function () {
64 this.timeout(60000)
65
66 await flushTests()
67
68 servers = await flushAndRunMultipleServers(2)
69 await setAccessTokensToServers(servers)
70
71 {
72 const user = { username: 'user1', password: 'password' }
73 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
74
75 userToken1 = await userLogin(servers[0], user)
76 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
77 }
78
79 {
80 const user = { username: 'user2', password: 'password' }
81 await createUser(servers[1].url, servers[1].accessToken, user.username, user.password)
82
83 userToken2 = await userLogin(servers[1], user)
84 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
85 }
86
87 {
88 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
89 videoUUID1 = res.body.video.uuid
90 }
91
92 {
93 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
94 videoUUID2 = res.body.video.uuid
95 }
96
97 await doubleFollow(servers[0], servers[1])
98
99 {
100 const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1')
101 const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1')
102 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1')
103 }
104
105 {
106 const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1')
107 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1')
108 }
109
110 await waitJobs(servers)
111 })
112
113 describe('When managing account blocklist', function () {
114 it('Should list all videos', function () {
115 return checkAllVideos(servers[0].url, servers[0].accessToken)
116 })
117
118 it('Should list the comments', function () {
119 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
120 })
121
122 it('Should block a remote account', async function () {
123 await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:9002')
124 })
125
126 it('Should hide its videos', async function () {
127 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
128
129 const videos: Video[] = res.body.data
130 expect(videos).to.have.lengthOf(3)
131
132 const v = videos.find(v => v.name === 'video user 2')
133 expect(v).to.be.undefined
134 })
135
136 it('Should block a local account', async function () {
137 await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
138 })
139
140 it('Should hide its videos', async function () {
141 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
142
143 const videos: Video[] = res.body.data
144 expect(videos).to.have.lengthOf(2)
145
146 const v = videos.find(v => v.name === 'video user 1')
147 expect(v).to.be.undefined
148 })
149
150 it('Should hide its comments', async function () {
151 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', servers[0].accessToken)
152
153 const threads: VideoComment[] = resThreads.body.data
154 expect(threads).to.have.lengthOf(1)
155 expect(threads[0].totalReplies).to.equal(0)
156
157 const t = threads.find(t => t.text === 'comment user 1')
158 expect(t).to.be.undefined
159
160 for (const thread of threads) {
161 const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, servers[0].accessToken)
162
163 const tree: VideoCommentThreadTree = res.body
164 expect(tree.children).to.have.lengthOf(0)
165 }
166 })
167
168 it('Should list all the videos with another user', async function () {
169 return checkAllVideos(servers[0].url, userToken1)
170 })
171
172 it('Should list all the comments with another user', async function () {
173 return checkAllComments(servers[0].url, userToken1, videoUUID1)
174 })
175
176 it('Should list blocked accounts', async function () {
177 {
178 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
179 const blocks: AccountBlock[] = res.body.data
180
181 expect(res.body.total).to.equal(2)
182
183 const block = blocks[0]
184 expect(block.byAccount.displayName).to.equal('root')
185 expect(block.byAccount.name).to.equal('root')
186 expect(block.accountBlocked.displayName).to.equal('user2')
187 expect(block.accountBlocked.name).to.equal('user2')
188 expect(block.accountBlocked.host).to.equal('localhost:9002')
189 }
190
191 {
192 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt')
193 const blocks: AccountBlock[] = res.body.data
194
195 expect(res.body.total).to.equal(2)
196
197 const block = blocks[0]
198 expect(block.byAccount.displayName).to.equal('root')
199 expect(block.byAccount.name).to.equal('root')
200 expect(block.accountBlocked.displayName).to.equal('user1')
201 expect(block.accountBlocked.name).to.equal('user1')
202 expect(block.accountBlocked.host).to.equal('localhost:9001')
203 }
204 })
205
206 it('Should unblock the remote account', async function () {
207 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:9002')
208 })
209
210 it('Should display its videos', async function () {
211 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
212
213 const videos: Video[] = res.body.data
214 expect(videos).to.have.lengthOf(3)
215
216 const v = videos.find(v => v.name === 'video user 2')
217 expect(v).not.to.be.undefined
218 })
219
220 it('Should unblock the local account', async function () {
221 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
222 })
223
224 it('Should display its comments', function () {
225 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
226 })
227 })
228
229 describe('When managing server blocklist', function () {
230 it('Should list all videos', function () {
231 return checkAllVideos(servers[0].url, servers[0].accessToken)
232 })
233
234 it('Should list the comments', function () {
235 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
236 })
237
238 it('Should block a remote server', async function () {
239 await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:9002')
240 })
241
242 it('Should hide its videos', async function () {
243 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
244
245 const videos: Video[] = res.body.data
246 expect(videos).to.have.lengthOf(2)
247
248 const v1 = videos.find(v => v.name === 'video user 2')
249 const v2 = videos.find(v => v.name === 'video server 2')
250
251 expect(v1).to.be.undefined
252 expect(v2).to.be.undefined
253 })
254
255 it('Should list all the videos with another user', async function () {
256 return checkAllVideos(servers[0].url, userToken1)
257 })
258
259 it('Should hide its comments')
260
261 it('Should list blocked servers', async function () {
262 const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
263 const blocks: ServerBlock[] = res.body.data
264
265 expect(res.body.total).to.equal(1)
266
267 const block = blocks[0]
268 expect(block.byAccount.displayName).to.equal('root')
269 expect(block.byAccount.name).to.equal('root')
270 expect(block.serverBlocked.host).to.equal('localhost:9002')
271 })
272
273 it('Should unblock the remote server', async function () {
274 await removeServerFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:9002')
275 })
276
277 it('Should display its videos', function () {
278 return checkAllVideos(servers[0].url, servers[0].accessToken)
279 })
280
281 it('Should display its comments', function () {
282 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
283 })
284 })
285
286 after(async function () {
287 killallServers(servers)
288
289 // Keep the logs if the test failed
290 if (this[ 'ok' ]) {
291 await flushTests()
292 }
293 })
294})
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/users/blocklist.ts b/server/tests/utils/users/blocklist.ts
new file mode 100644
index 000000000..47b315480
--- /dev/null
+++ b/server/tests/utils/users/blocklist.ts
@@ -0,0 +1,103 @@
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
94// ---------------------------------------------------------------------------
95
96export {
97 getAccountBlocklistByAccount,
98 addAccountToAccountBlocklist,
99 removeAccountFromAccountBlocklist,
100 getServerBlocklistByAccount,
101 addServerToAccountBlocklist,
102 removeServerFromAccountBlocklist
103}
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/shared/models/blocklist/account-block.model.ts b/shared/models/blocklist/account-block.model.ts
new file mode 100644
index 000000000..d6f8840c5
--- /dev/null
+++ b/shared/models/blocklist/account-block.model.ts
@@ -0,0 +1,7 @@
1import { Account } from '../actors'
2
3export interface AccountBlock {
4 byAccount: Account
5 accountBlocked: Account
6 createdAt: Date | string
7}
diff --git a/shared/models/blocklist/index.ts b/shared/models/blocklist/index.ts
new file mode 100644
index 000000000..fc7873270
--- /dev/null
+++ b/shared/models/blocklist/index.ts
@@ -0,0 +1,2 @@
1export * from './account-block.model'
2export * from './server-block.model'
diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/blocklist/server-block.model.ts
new file mode 100644
index 000000000..efba672bd
--- /dev/null
+++ b/shared/models/blocklist/server-block.model.ts
@@ -0,0 +1,9 @@
1import { Account } from '../actors'
2
3export interface ServerBlock {
4 byAccount: Account
5 serverBlocked: {
6 host: string
7 }
8 createdAt: Date | string
9}
diff --git a/shared/models/index.ts b/shared/models/index.ts
index e61d6cbdc..062533834 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -1,6 +1,7 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './actors' 2export * from './actors'
3export * from './avatars' 3export * from './avatars'
4export * from './blocklist'
4export * from './redundancy' 5export * from './redundancy'
5export * from './users' 6export * from './users'
6export * from './videos' 7export * from './videos'