aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts2
-rw-r--r--server/controllers/api/bulk.ts41
-rw-r--r--server/controllers/api/config.ts13
-rw-r--r--server/controllers/api/index.ts20
-rw-r--r--server/controllers/api/videos/comment.ts35
-rw-r--r--server/helpers/custom-validators/bulk.ts9
-rw-r--r--server/initializers/checker-after-init.ts14
-rw-r--r--server/initializers/config.ts7
-rw-r--r--server/lib/activitypub/process/process-create.ts23
-rw-r--r--server/lib/activitypub/send/send-delete.ts12
-rw-r--r--server/lib/blocklist.ts25
-rw-r--r--server/lib/notifier.ts55
-rw-r--r--server/lib/video-comment.ts29
-rw-r--r--server/middlewares/validators/blocklist.ts12
-rw-r--r--server/middlewares/validators/bulk.ts41
-rw-r--r--server/middlewares/validators/config.ts5
-rw-r--r--server/models/account/account.ts26
-rw-r--r--server/models/utils.ts5
-rw-r--r--server/models/video/video-abuse.ts2
-rw-r--r--server/models/video/video-comment.ts191
-rw-r--r--server/tests/api/check-params/bulk.ts88
-rw-r--r--server/tests/api/check-params/config.ts6
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/server/bulk.ts198
-rw-r--r--server/tests/api/server/config.ts16
-rw-r--r--server/tests/api/users/blocklist.ts178
-rw-r--r--server/tests/api/videos/multiple-servers.ts9
-rw-r--r--server/tests/feeds/feeds.ts41
28 files changed, 925 insertions, 179 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index f94abf808..e48641836 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -285,7 +285,7 @@ async function videoCommentsController (req: express.Request, res: express.Respo
285 const video = res.locals.onlyImmutableVideo 285 const video = res.locals.onlyImmutableVideo
286 286
287 const handler = async (start: number, count: number) => { 287 const handler = async (start: number, count: number) => {
288 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) 288 const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count)
289 return { 289 return {
290 total: result.count, 290 total: result.count,
291 data: result.rows.map(r => r.url) 291 data: result.rows.map(r => r.url)
diff --git a/server/controllers/api/bulk.ts b/server/controllers/api/bulk.ts
new file mode 100644
index 000000000..1fe139c92
--- /dev/null
+++ b/server/controllers/api/bulk.ts
@@ -0,0 +1,41 @@
1import * as express from 'express'
2import { asyncMiddleware, authenticate } from '../../middlewares'
3import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk'
4import { VideoCommentModel } from '@server/models/video/video-comment'
5import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
6import { removeComment } from '@server/lib/video-comment'
7
8const bulkRouter = express.Router()
9
10bulkRouter.post('/remove-comments-of',
11 authenticate,
12 asyncMiddleware(bulkRemoveCommentsOfValidator),
13 asyncMiddleware(bulkRemoveCommentsOf)
14)
15
16// ---------------------------------------------------------------------------
17
18export {
19 bulkRouter
20}
21
22// ---------------------------------------------------------------------------
23
24async function bulkRemoveCommentsOf (req: express.Request, res: express.Response) {
25 const account = res.locals.account
26 const body = req.body as BulkRemoveCommentsOfBody
27 const user = res.locals.oauth.token.User
28
29 const filter = body.scope === 'my-videos'
30 ? { onVideosOfAccount: user.Account }
31 : {}
32
33 const comments = await VideoCommentModel.listForBulkDelete(account, filter)
34
35 // Don't wait result
36 res.sendStatus(204)
37
38 for (const comment of comments) {
39 await removeComment(comment)
40 }
41}
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index edcb0b99e..41e5027b9 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -172,6 +172,13 @@ async function getConfig (req: express.Request, res: express.Response) {
172 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL 172 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
173 } 173 }
174 } 174 }
175 },
176
177 broadcastMessage: {
178 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
179 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
180 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
181 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
175 } 182 }
176 } 183 }
177 184
@@ -432,6 +439,12 @@ function customConfig (): CustomConfig {
432 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL 439 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
433 } 440 }
434 } 441 }
442 },
443 broadcastMessage: {
444 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
445 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
446 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
447 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
435 } 448 }
436 } 449 }
437} 450}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 7bec6c527..c334a26b4 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,20 +1,21 @@
1import * as cors from 'cors'
1import * as express from 'express' 2import * as express from 'express'
3import * as RateLimit from 'express-rate-limit'
4import { badRequest } from '../../helpers/express-utils'
5import { CONFIG } from '../../initializers/config'
6import { accountsRouter } from './accounts'
7import { bulkRouter } from './bulk'
2import { configRouter } from './config' 8import { configRouter } from './config'
3import { jobsRouter } from './jobs' 9import { jobsRouter } from './jobs'
4import { oauthClientsRouter } from './oauth-clients' 10import { oauthClientsRouter } from './oauth-clients'
11import { overviewsRouter } from './overviews'
12import { pluginRouter } from './plugins'
13import { searchRouter } from './search'
5import { serverRouter } from './server' 14import { serverRouter } from './server'
6import { usersRouter } from './users' 15import { usersRouter } from './users'
7import { accountsRouter } from './accounts'
8import { videosRouter } from './videos'
9import { badRequest } from '../../helpers/express-utils'
10import { videoChannelRouter } from './video-channel' 16import { videoChannelRouter } from './video-channel'
11import * as cors from 'cors'
12import { searchRouter } from './search'
13import { overviewsRouter } from './overviews'
14import { videoPlaylistRouter } from './video-playlist' 17import { videoPlaylistRouter } from './video-playlist'
15import { CONFIG } from '../../initializers/config' 18import { videosRouter } from './videos'
16import { pluginRouter } from './plugins'
17import * as RateLimit from 'express-rate-limit'
18 19
19const apiRouter = express.Router() 20const apiRouter = express.Router()
20 21
@@ -31,6 +32,7 @@ const apiRateLimiter = RateLimit({
31apiRouter.use(apiRateLimiter) 32apiRouter.use(apiRateLimiter)
32 33
33apiRouter.use('/server', serverRouter) 34apiRouter.use('/server', serverRouter)
35apiRouter.use('/bulk', bulkRouter)
34apiRouter.use('/oauth-clients', oauthClientsRouter) 36apiRouter.use('/oauth-clients', oauthClientsRouter)
35apiRouter.use('/config', configRouter) 37apiRouter.use('/config', configRouter)
36apiRouter.use('/users', usersRouter) 38apiRouter.use('/users', usersRouter)
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 5070bb3c0..45ff969d9 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -1,11 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { cloneDeep } from 'lodash'
3import { ResultList } from '../../../../shared/models' 2import { ResultList } from '../../../../shared/models'
4import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' 3import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
5import { logger } from '../../../helpers/logger' 4import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
6import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
7import { sequelizeTypescript } from '../../../initializers/database' 6import { sequelizeTypescript } from '../../../initializers/database'
8import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment' 7import { Notifier } from '../../../lib/notifier'
8import { Hooks } from '../../../lib/plugins/hooks'
9import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment'
9import { 10import {
10 asyncMiddleware, 11 asyncMiddleware,
11 asyncRetryTransactionMiddleware, 12 asyncRetryTransactionMiddleware,
@@ -23,12 +24,8 @@ import {
23 removeVideoCommentValidator, 24 removeVideoCommentValidator,
24 videoCommentThreadsSortValidator 25 videoCommentThreadsSortValidator
25} from '../../../middlewares/validators' 26} from '../../../middlewares/validators'
26import { VideoCommentModel } from '../../../models/video/video-comment'
27import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
28import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
29import { Notifier } from '../../../lib/notifier' 28import { VideoCommentModel } from '../../../models/video/video-comment'
30import { Hooks } from '../../../lib/plugins/hooks'
31import { sendDeleteVideoComment } from '../../../lib/activitypub/send'
32 29
33const auditLogger = auditLoggerFactory('comments') 30const auditLogger = auditLoggerFactory('comments')
34const videoCommentRouter = express.Router() 31const videoCommentRouter = express.Router()
@@ -81,6 +78,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
81 if (video.commentsEnabled === true) { 78 if (video.commentsEnabled === true) {
82 const apiOptions = await Hooks.wrapObject({ 79 const apiOptions = await Hooks.wrapObject({
83 videoId: video.id, 80 videoId: video.id,
81 isVideoOwned: video.isOwned(),
84 start: req.query.start, 82 start: req.query.start,
85 count: req.query.count, 83 count: req.query.count,
86 sort: req.query.sort, 84 sort: req.query.sort,
@@ -111,6 +109,7 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
111 if (video.commentsEnabled === true) { 109 if (video.commentsEnabled === true) {
112 const apiOptions = await Hooks.wrapObject({ 110 const apiOptions = await Hooks.wrapObject({
113 videoId: video.id, 111 videoId: video.id,
112 isVideoOwned: video.isOwned(),
114 threadId: res.locals.videoCommentThread.id, 113 threadId: res.locals.videoCommentThread.id,
115 user 114 user
116 }, 'filter:api.video-thread-comments.list.params') 115 }, 'filter:api.video-thread-comments.list.params')
@@ -149,9 +148,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
149 148
150 Hooks.runAction('action:api.video-thread.created', { comment }) 149 Hooks.runAction('action:api.video-thread.created', { comment })
151 150
152 return res.json({ 151 return res.json({ comment: comment.toFormattedJSON() })
153 comment: comment.toFormattedJSON()
154 }).end()
155} 152}
156 153
157async function addVideoCommentReply (req: express.Request, res: express.Response) { 154async function addVideoCommentReply (req: express.Request, res: express.Response) {
@@ -173,27 +170,15 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
173 170
174 Hooks.runAction('action:api.video-comment-reply.created', { comment }) 171 Hooks.runAction('action:api.video-comment-reply.created', { comment })
175 172
176 return res.json({ comment: comment.toFormattedJSON() }).end() 173 return res.json({ comment: comment.toFormattedJSON() })
177} 174}
178 175
179async function removeVideoComment (req: express.Request, res: express.Response) { 176async function removeVideoComment (req: express.Request, res: express.Response) {
180 const videoCommentInstance = res.locals.videoCommentFull 177 const videoCommentInstance = res.locals.videoCommentFull
181 const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
182
183 await sequelizeTypescript.transaction(async t => {
184 if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
185 await sendDeleteVideoComment(videoCommentInstance, t)
186 }
187 178
188 markCommentAsDeleted(videoCommentInstance) 179 await removeComment(videoCommentInstance)
189
190 await videoCommentInstance.save()
191 })
192 180
193 auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON())) 181 auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
194 logger.info('Video comment %d deleted.', videoCommentInstance.id)
195
196 Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
197 182
198 return res.type('json').status(204).end() 183 return res.type('json').status(204).end()
199} 184}
diff --git a/server/helpers/custom-validators/bulk.ts b/server/helpers/custom-validators/bulk.ts
new file mode 100644
index 000000000..9e0ce0be1
--- /dev/null
+++ b/server/helpers/custom-validators/bulk.ts
@@ -0,0 +1,9 @@
1function isBulkRemoveCommentsOfScopeValid (value: string) {
2 return value === 'my-videos' || value === 'instance'
3}
4
5// ---------------------------------------------------------------------------
6
7export {
8 isBulkRemoveCommentsOfScopeValid
9}
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index f111be2ae..b5b854137 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -107,6 +107,10 @@ function checkConfig () {
107 } 107 }
108 } 108 }
109 109
110 if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) {
111 logger.warn('Redundancy directory should be different than the videos folder.')
112 }
113
110 // Transcoding 114 // Transcoding
111 if (CONFIG.TRANSCODING.ENABLED) { 115 if (CONFIG.TRANSCODING.ENABLED) {
112 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { 116 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
@@ -114,8 +118,14 @@ function checkConfig () {
114 } 118 }
115 } 119 }
116 120
117 if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { 121 // Broadcast message
118 logger.warn('Redundancy directory should be different than the videos folder.') 122 if (CONFIG.BROADCAST_MESSAGE.ENABLED) {
123 const currentLevel = CONFIG.BROADCAST_MESSAGE.LEVEL
124 const available = [ 'info', 'warning', 'error' ]
125
126 if (available.includes(currentLevel) === false) {
127 return 'Broadcast message level should be ' + available.join(' or ') + ' instead of ' + currentLevel
128 }
119 } 129 }
120 130
121 return null 131 return null
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 6932b41e1..e2920ce9e 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -6,6 +6,7 @@ import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-
6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
7import * as bytes from 'bytes' 7import * as bytes from 'bytes'
8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { BroadcastMessageLevel } from '@shared/models/server'
9 10
10// Use a variable to reload the configuration if we need 11// Use a variable to reload the configuration if we need
11let config: IConfig = require('config') 12let config: IConfig = require('config')
@@ -285,6 +286,12 @@ const CONFIG = {
285 }, 286 },
286 THEME: { 287 THEME: {
287 get DEFAULT () { return config.get<string>('theme.default') } 288 get DEFAULT () { return config.get<string>('theme.default') }
289 },
290 BROADCAST_MESSAGE: {
291 get ENABLED () { return config.get<boolean>('broadcast_message.enabled') },
292 get MESSAGE () { return config.get<string>('broadcast_message.message') },
293 get LEVEL () { return config.get<BroadcastMessageLevel>('broadcast_message.level') },
294 get DISMISSABLE () { return config.get<boolean>('broadcast_message.dismissable') }
288 } 295 }
289} 296}
290 297
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 566bf6992..f8f9b80c6 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -1,18 +1,19 @@
1import { isRedundancyAccepted } from '@server/lib/redundancy'
1import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' 2import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared'
3import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 4import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 5import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers/database' 7import { sequelizeTypescript } from '../../../initializers/database'
6import { resolveThread } from '../video-comments'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { forwardVideoRelatedActivity } from '../send/utils'
9import { createOrUpdateCacheFile } from '../cache-file'
10import { Notifier } from '../../notifier'
11import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
12import { createOrUpdateVideoPlaylist } from '../playlist'
13import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 8import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' 9import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
15import { isRedundancyAccepted } from '@server/lib/redundancy' 10import { Notifier } from '../../notifier'
11import { createOrUpdateCacheFile } from '../cache-file'
12import { createOrUpdateVideoPlaylist } from '../playlist'
13import { forwardVideoRelatedActivity } from '../send/utils'
14import { resolveThread } from '../video-comments'
15import { getOrCreateVideoAndAccountAndChannel } from '../videos'
16import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
16 17
17async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { 18async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
18 const { activity, byActor } = options 19 const { activity, byActor } = options
@@ -101,6 +102,12 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc
101 return 102 return
102 } 103 }
103 104
105 // Try to not forward unwanted commments on our videos
106 if (video.isOwned() && await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) {
107 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url)
108 return
109 }
110
104 if (video.isOwned() && created === true) { 111 if (video.isOwned() && created === true) {
105 // Don't resend the activity to the sender 112 // Don't resend the activity to the sender
106 const exceptions = [ byActor ] 113 const exceptions = [ byActor ]
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index fd3f06dec..2afd2c05d 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -1,15 +1,15 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application'
2import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' 3import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
4import { logger } from '../../../helpers/logger'
3import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
4import { VideoCommentModel } from '../../../models/video/video-comment' 6import { VideoCommentModel } from '../../../models/video/video-comment'
5import { VideoShareModel } from '../../../models/video/video-share' 7import { VideoShareModel } from '../../../models/video/video-share'
8import { MActorUrl } from '../../../typings/models'
9import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
10import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
6import { getDeleteActivityPubUrl } from '../url' 11import { getDeleteActivityPubUrl } from '../url'
7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 12import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
9import { logger } from '../../../helpers/logger'
10import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
11import { MActorUrl } from '../../../typings/models'
12import { getServerActor } from '@server/models/application/application'
13 13
14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { 14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
15 logger.info('Creating job to broadcast delete of video %s.', video.url) 15 logger.info('Creating job to broadcast delete of video %s.', video.url)
@@ -42,7 +42,7 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
42 return broadcastToFollowers(activity, byActor, actorsInvolved, t) 42 return broadcastToFollowers(activity, byActor, actorsInvolved, t)
43} 43}
44 44
45async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t: Transaction) { 45async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, t: Transaction) {
46 logger.info('Creating job to send delete of comment %s.', videoComment.url) 46 logger.info('Creating job to send delete of comment %s.', videoComment.url)
47 47
48 const isVideoOrigin = videoComment.Video.isOwned() 48 const isVideoOrigin = videoComment.Video.isOwned()
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index 842eecb5b..d282d091b 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -1,5 +1,6 @@
1import { sequelizeTypescript } from '@server/initializers/database' 1import { sequelizeTypescript } from '@server/initializers/database'
2import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models' 2import { getServerActor } from '@server/models/application/application'
3import { MAccountBlocklist, MAccountId, MAccountServer, MServerBlocklist } from '@server/typings/models'
3import { AccountBlocklistModel } from '../models/account/account-blocklist' 4import { AccountBlocklistModel } from '../models/account/account-blocklist'
4import { ServerBlocklistModel } from '../models/server/server-blocklist' 5import { ServerBlocklistModel } from '../models/server/server-blocklist'
5 6
@@ -33,9 +34,29 @@ function removeServerFromBlocklist (serverBlock: MServerBlocklist) {
33 }) 34 })
34} 35}
35 36
37async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAccount?: MAccountId) {
38 const serverAccountId = (await getServerActor()).Account.id
39 const sourceAccounts = [ serverAccountId ]
40
41 if (userAccount) sourceAccounts.push(userAccount.id)
42
43 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id)
44 if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) {
45 return true
46 }
47
48 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId)
49 if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) {
50 return true
51 }
52
53 return false
54}
55
36export { 56export {
37 addAccountInBlocklist, 57 addAccountInBlocklist,
38 addServerInBlocklist, 58 addServerInBlocklist,
39 removeAccountFromBlocklist, 59 removeAccountFromBlocklist,
40 removeServerFromBlocklist 60 removeServerFromBlocklist,
61 isBlockedByServerOrAccount
41} 62}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 017739523..89f91e031 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -1,12 +1,22 @@
1import { getServerActor } from '@server/models/application/application'
2import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
3import {
4 MUser,
5 MUserAccount,
6 MUserDefault,
7 MUserNotifSettingAccount,
8 MUserWithNotificationSetting,
9 UserNotificationModelForApi
10} from '@server/typings/models/user'
11import { MVideoImportVideo } from '@server/typings/models/video/video-import'
1import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' 12import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
13import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos'
2import { logger } from '../helpers/logger' 14import { logger } from '../helpers/logger'
3import { Emailer } from './emailer'
4import { UserNotificationModel } from '../models/account/user-notification'
5import { UserModel } from '../models/account/user'
6import { PeerTubeSocket } from './peertube-socket'
7import { CONFIG } from '../initializers/config' 15import { CONFIG } from '../initializers/config'
8import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos'
9import { AccountBlocklistModel } from '../models/account/account-blocklist' 16import { AccountBlocklistModel } from '../models/account/account-blocklist'
17import { UserModel } from '../models/account/user'
18import { UserNotificationModel } from '../models/account/user-notification'
19import { MAccountServer, MActorFollowFull } from '../typings/models'
10import { 20import {
11 MCommentOwnerVideo, 21 MCommentOwnerVideo,
12 MVideoAbuseVideo, 22 MVideoAbuseVideo,
@@ -15,18 +25,9 @@ import {
15 MVideoBlacklistVideo, 25 MVideoBlacklistVideo,
16 MVideoFullLight 26 MVideoFullLight
17} from '../typings/models/video' 27} from '../typings/models/video'
18import { 28import { isBlockedByServerOrAccount } from './blocklist'
19 MUser, 29import { Emailer } from './emailer'
20 MUserAccount, 30import { PeerTubeSocket } from './peertube-socket'
21 MUserDefault,
22 MUserNotifSettingAccount,
23 MUserWithNotificationSetting,
24 UserNotificationModelForApi
25} from '@server/typings/models/user'
26import { MAccountDefault, MActorFollowFull } from '../typings/models'
27import { MVideoImportVideo } from '@server/typings/models/video/video-import'
28import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
29import { getServerActor } from '@server/models/application/application'
30 31
31class Notifier { 32class Notifier {
32 33
@@ -169,7 +170,7 @@ class Notifier {
169 // Not our user or user comments its own video 170 // Not our user or user comments its own video
170 if (!user || comment.Account.userId === user.id) return 171 if (!user || comment.Account.userId === user.id) return
171 172
172 if (await this.isBlockedByServerOrAccount(user, comment.Account)) return 173 if (await this.isBlockedByServerOrUser(comment.Account, user)) return
173 174
174 logger.info('Notifying user %s of new comment %s.', user.username, comment.url) 175 logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
175 176
@@ -270,7 +271,7 @@ class Notifier {
270 const followerAccount = actorFollow.ActorFollower.Account 271 const followerAccount = actorFollow.ActorFollower.Account
271 const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower }) 272 const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower })
272 273
273 if (await this.isBlockedByServerOrAccount(user, followerAccountWithActor)) return 274 if (await this.isBlockedByServerOrUser(followerAccountWithActor, user)) return
274 275
275 logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) 276 logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
276 277
@@ -299,6 +300,9 @@ class Notifier {
299 private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) { 300 private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
300 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) 301 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
301 302
303 const follower = Object.assign(actorFollow.ActorFollower.Account, { Actor: actorFollow.ActorFollower })
304 if (await this.isBlockedByServerOrUser(follower)) return
305
302 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) 306 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
303 307
304 function settingGetter (user: MUserWithNotificationSetting) { 308 function settingGetter (user: MUserWithNotificationSetting) {
@@ -590,17 +594,8 @@ class Notifier {
590 return value & UserNotificationSettingValue.WEB 594 return value & UserNotificationSettingValue.WEB
591 } 595 }
592 596
593 private async isBlockedByServerOrAccount (user: MUserAccount, targetAccount: MAccountDefault) { 597 private isBlockedByServerOrUser (targetAccount: MAccountServer, user?: MUserAccount) {
594 const serverAccountId = (await getServerActor()).Account.id 598 return isBlockedByServerOrAccount(targetAccount, user?.Account)
595 const sourceAccounts = [ serverAccountId, user.Account.id ]
596
597 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id)
598 if (accountMutedHash[serverAccountId] || accountMutedHash[user.Account.id]) return true
599
600 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId)
601 if (instanceMutedHash[serverAccountId] || instanceMutedHash[user.Account.id]) return true
602
603 return false
604 } 599 }
605 600
606 static get Instance () { 601 static get Instance () {
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index 516c912a9..97aa639fb 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -1,10 +1,32 @@
1import { cloneDeep } from 'lodash'
1import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import { logger } from '@server/helpers/logger'
4import { sequelizeTypescript } from '@server/initializers/database'
2import { ResultList } from '../../shared/models' 5import { ResultList } from '../../shared/models'
3import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' 6import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
4import { VideoCommentModel } from '../models/video/video-comment' 7import { VideoCommentModel } from '../models/video/video-comment'
8import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight, MCommentOwnerVideo } from '../typings/models'
9import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send'
5import { getVideoCommentActivityPubUrl } from './activitypub/url' 10import { getVideoCommentActivityPubUrl } from './activitypub/url'
6import { sendCreateVideoComment } from './activitypub/send' 11import { Hooks } from './plugins/hooks'
7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' 12
13async function removeComment (videoCommentInstance: MCommentOwnerVideo) {
14 const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
15
16 await sequelizeTypescript.transaction(async t => {
17 if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
18 await sendDeleteVideoComment(videoCommentInstance, t)
19 }
20
21 markCommentAsDeleted(videoCommentInstance)
22
23 await videoCommentInstance.save()
24 })
25
26 logger.info('Video comment %d deleted.', videoCommentInstance.id)
27
28 Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
29}
8 30
9async function createVideoComment (obj: { 31async function createVideoComment (obj: {
10 text: string 32 text: string
@@ -73,7 +95,7 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
73 return thread 95 return thread
74} 96}
75 97
76function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void { 98function markCommentAsDeleted (comment: MComment): void {
77 comment.text = '' 99 comment.text = ''
78 comment.deletedAt = new Date() 100 comment.deletedAt = new Date()
79 comment.accountId = null 101 comment.accountId = null
@@ -82,6 +104,7 @@ function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
82// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
83 105
84export { 106export {
107 removeComment,
85 createVideoComment, 108 createVideoComment,
86 buildFormattedCommentTree, 109 buildFormattedCommentTree,
87 markCommentAsDeleted 110 markCommentAsDeleted
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index 27224ff9b..c24fa9609 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -24,8 +24,7 @@ const blockAccountValidator = [
24 24
25 if (user.Account.id === accountToBlock.id) { 25 if (user.Account.id === accountToBlock.id) {
26 res.status(409) 26 res.status(409)
27 .send({ error: 'You cannot block yourself.' }) 27 .json({ error: 'You cannot block yourself.' })
28 .end()
29 28
30 return 29 return
31 } 30 }
@@ -80,8 +79,7 @@ const blockServerValidator = [
80 79
81 if (host === WEBSERVER.HOST) { 80 if (host === WEBSERVER.HOST) {
82 return res.status(409) 81 return res.status(409)
83 .send({ error: 'You cannot block your own server.' }) 82 .json({ error: 'You cannot block your own server.' })
84 .end()
85 } 83 }
86 84
87 const server = await ServerModel.loadOrCreateByHost(host) 85 const server = await ServerModel.loadOrCreateByHost(host)
@@ -139,8 +137,7 @@ async function doesUnblockAccountExist (accountId: number, targetAccountId: numb
139 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) 137 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
140 if (!accountBlock) { 138 if (!accountBlock) {
141 res.status(404) 139 res.status(404)
142 .send({ error: 'Account block entry not found.' }) 140 .json({ error: 'Account block entry not found.' })
143 .end()
144 141
145 return false 142 return false
146 } 143 }
@@ -154,8 +151,7 @@ async function doesUnblockServerExist (accountId: number, host: string, res: exp
154 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) 151 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
155 if (!serverBlock) { 152 if (!serverBlock) {
156 res.status(404) 153 res.status(404)
157 .send({ error: 'Server block entry not found.' }) 154 .json({ error: 'Server block entry not found.' })
158 .end()
159 155
160 return false 156 return false
161 } 157 }
diff --git a/server/middlewares/validators/bulk.ts b/server/middlewares/validators/bulk.ts
new file mode 100644
index 000000000..f9b0f565a
--- /dev/null
+++ b/server/middlewares/validators/bulk.ts
@@ -0,0 +1,41 @@
1import * as express from 'express'
2import { body } from 'express-validator'
3import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk'
4import { doesAccountNameWithHostExist } from '@server/helpers/middlewares'
5import { UserRight } from '@shared/models'
6import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
7import { logger } from '../../helpers/logger'
8import { areValidationErrors } from './utils'
9
10const bulkRemoveCommentsOfValidator = [
11 body('accountName').exists().withMessage('Should have an account name with host'),
12 body('scope')
13 .custom(isBulkRemoveCommentsOfScopeValid).withMessage('Should have a valid scope'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking bulkRemoveCommentsOfValidator parameters', { parameters: req.body })
17
18 if (areValidationErrors(req, res)) return
19 if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
20
21 const user = res.locals.oauth.token.User
22 const body = req.body as BulkRemoveCommentsOfBody
23
24 if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
25 return res.status(403)
26 .json({
27 error: 'User cannot remove any comments of this instance.'
28 })
29 }
30
31 return next()
32 }
33]
34
35// ---------------------------------------------------------------------------
36
37export {
38 bulkRemoveCommentsOfValidator
39}
40
41// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index dfa549e76..6905ac762 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -55,6 +55,11 @@ const customConfigUpdateValidator = [
55 55
56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'), 56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
57 57
58 body('broadcastMessage.enabled').isBoolean().withMessage('Should have a valid broadcast message enabled boolean'),
59 body('broadcastMessage.message').exists().withMessage('Should have a valid broadcast message'),
60 body('broadcastMessage.level').exists().withMessage('Should have a valid broadcast level'),
61 body('broadcastMessage.dismissable').exists().withMessage('Should have a valid broadcast dismissable boolean'),
62
58 (req: express.Request, res: express.Response, next: express.NextFunction) => { 63 (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) 64 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
60 65
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index a0081f259..ad649837a 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -32,9 +32,10 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
32import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
33import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models' 35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable, MAccount } from '../../typings/models'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
37import { ModelCache } from '@server/models/model-cache' 37import { ModelCache } from '@server/models/model-cache'
38import { VideoModel } from '../video/video'
38 39
39export enum ScopeNames { 40export enum ScopeNames {
40 SUMMARY = 'SUMMARY' 41 SUMMARY = 'SUMMARY'
@@ -343,6 +344,29 @@ export class AccountModel extends Model<AccountModel> {
343 }) 344 })
344 } 345 }
345 346
347 static loadAccountIdFromVideo (videoId: number): Bluebird<MAccount> {
348 const query = {
349 include: [
350 {
351 attributes: [ 'id', 'accountId' ],
352 model: VideoChannelModel.unscoped(),
353 required: true,
354 include: [
355 {
356 attributes: [ 'id', 'channelId' ],
357 model: VideoModel.unscoped(),
358 where: {
359 id: videoId
360 }
361 }
362 ]
363 }
364 ]
365 }
366
367 return AccountModel.findOne(query)
368 }
369
346 static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> { 370 static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> {
347 const query = { 371 const query = {
348 attributes: [ ], 372 attributes: [ ],
diff --git a/server/models/utils.ts b/server/models/utils.ts
index b2573cd35..88c9b4adb 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -136,10 +136,7 @@ function createSimilarityAttribute (col: string, value: string) {
136 ) 136 )
137} 137}
138 138
139function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { 139function buildBlockedAccountSQL (blockerIds: number[]) {
140 const blockerIds = [ serverAccountId ]
141 if (userAccountId) blockerIds.push(userAccountId)
142
143 const blockerIdsString = blockerIds.join(', ') 140 const blockerIdsString = blockerIds.join(', ')
144 141
145 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + 142 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 0844f702d..e0cf50b59 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -57,7 +57,7 @@ export enum ScopeNames {
57 }) => { 57 }) => {
58 const where = { 58 const where = {
59 reporterAccountId: { 59 reporterAccountId: {
60 [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') 60 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
61 } 61 }
62 } 62 }
63 63
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 6d60271e6..ba09522cc 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,19 +1,17 @@
1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
5import { getServerActor } from '@server/models/application/application'
6import { MAccount, MAccountId, MUserAccountId } from '@server/typings/models'
7import { VideoPrivacy } from '@shared/models'
2import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 8import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
3import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 9import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
4import { VideoComment } from '../../../shared/models/videos/video-comment.model' 10import { VideoComment } from '../../../shared/models/videos/video-comment.model'
5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
6import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
7import { AccountModel } from '../account/account'
8import { ActorModel } from '../activitypub/actor'
9import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
10import { VideoModel } from './video'
11import { VideoChannelModel } from './video-channel'
12import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 11import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
13import { regexpCapture } from '../../helpers/regexp' 13import { regexpCapture } from '../../helpers/regexp'
14import { uniq } from 'lodash' 14import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
15import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
16import * as Bluebird from 'bluebird'
17import { 15import {
18 MComment, 16 MComment,
19 MCommentAP, 17 MCommentAP,
@@ -23,28 +21,32 @@ import {
23 MCommentOwnerReplyVideoLight, 21 MCommentOwnerReplyVideoLight,
24 MCommentOwnerVideo, 22 MCommentOwnerVideo,
25 MCommentOwnerVideoFeed, 23 MCommentOwnerVideoFeed,
26 MCommentOwnerVideoReply 24 MCommentOwnerVideoReply,
25 MVideoImmutable
27} from '../../typings/models/video' 26} from '../../typings/models/video'
28import { MUserAccountId } from '@server/typings/models' 27import { AccountModel } from '../account/account'
29import { VideoPrivacy } from '@shared/models' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
30import { getServerActor } from '@server/models/application/application' 29import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
30import { VideoModel } from './video'
31import { VideoChannelModel } from './video-channel'
31 32
32enum ScopeNames { 33enum ScopeNames {
33 WITH_ACCOUNT = 'WITH_ACCOUNT', 34 WITH_ACCOUNT = 'WITH_ACCOUNT',
35 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
34 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', 36 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
35 WITH_VIDEO = 'WITH_VIDEO', 37 WITH_VIDEO = 'WITH_VIDEO',
36 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' 38 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
37} 39}
38 40
39@Scopes(() => ({ 41@Scopes(() => ({
40 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { 42 [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => {
41 return { 43 return {
42 attributes: { 44 attributes: {
43 include: [ 45 include: [
44 [ 46 [
45 Sequelize.literal( 47 Sequelize.literal(
46 '(' + 48 '(' +
47 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + 49 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' +
48 'SELECT COUNT("replies"."id") - (' + 50 'SELECT COUNT("replies"."id") - (' +
49 'SELECT COUNT("replies"."id") ' + 51 'SELECT COUNT("replies"."id") ' +
50 'FROM "videoComment" AS "replies" ' + 52 'FROM "videoComment" AS "replies" ' +
@@ -82,6 +84,22 @@ enum ScopeNames {
82 } 84 }
83 ] 85 ]
84 }, 86 },
87 [ScopeNames.WITH_ACCOUNT_FOR_API]: {
88 include: [
89 {
90 model: AccountModel.unscoped(),
91 include: [
92 {
93 attributes: {
94 exclude: unusedActorAttributesForAPI
95 },
96 model: ActorModel, // Default scope includes avatar and server
97 required: true
98 }
99 ]
100 }
101 ]
102 },
85 [ScopeNames.WITH_IN_REPLY_TO]: { 103 [ScopeNames.WITH_IN_REPLY_TO]: {
86 include: [ 104 include: [
87 { 105 {
@@ -259,36 +277,50 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
259 277
260 static async listThreadsForApi (parameters: { 278 static async listThreadsForApi (parameters: {
261 videoId: number 279 videoId: number
280 isVideoOwned: boolean
262 start: number 281 start: number
263 count: number 282 count: number
264 sort: string 283 sort: string
265 user?: MUserAccountId 284 user?: MUserAccountId
266 }) { 285 }) {
267 const { videoId, start, count, sort, user } = parameters 286 const { videoId, isVideoOwned, start, count, sort, user } = parameters
268 287
269 const serverActor = await getServerActor() 288 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
270 const serverAccountId = serverActor.Account.id
271 const userAccountId = user ? user.Account.id : undefined
272 289
273 const query = { 290 const query = {
274 offset: start, 291 offset: start,
275 limit: count, 292 limit: count,
276 order: getCommentSort(sort), 293 order: getCommentSort(sort),
277 where: { 294 where: {
278 videoId, 295 [Op.and]: [
279 inReplyToCommentId: null, 296 {
280 accountId: { 297 videoId
281 [Op.notIn]: Sequelize.literal( 298 },
282 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 299 {
283 ) 300 inReplyToCommentId: null
284 } 301 },
302 {
303 [Op.or]: [
304 {
305 accountId: {
306 [Op.notIn]: Sequelize.literal(
307 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
308 )
309 }
310 },
311 {
312 accountId: null
313 }
314 ]
315 }
316 ]
285 } 317 }
286 } 318 }
287 319
288 const scopes: (string | ScopeOptions)[] = [ 320 const scopes: (string | ScopeOptions)[] = [
289 ScopeNames.WITH_ACCOUNT, 321 ScopeNames.WITH_ACCOUNT_FOR_API,
290 { 322 {
291 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 323 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
292 } 324 }
293 ] 325 ]
294 326
@@ -302,14 +334,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
302 334
303 static async listThreadCommentsForApi (parameters: { 335 static async listThreadCommentsForApi (parameters: {
304 videoId: number 336 videoId: number
337 isVideoOwned: boolean
305 threadId: number 338 threadId: number
306 user?: MUserAccountId 339 user?: MUserAccountId
307 }) { 340 }) {
308 const { videoId, threadId, user } = parameters 341 const { videoId, threadId, user, isVideoOwned } = parameters
309 342
310 const serverActor = await getServerActor() 343 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
311 const serverAccountId = serverActor.Account.id
312 const userAccountId = user ? user.Account.id : undefined
313 344
314 const query = { 345 const query = {
315 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 346 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
@@ -321,16 +352,16 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
321 ], 352 ],
322 accountId: { 353 accountId: {
323 [Op.notIn]: Sequelize.literal( 354 [Op.notIn]: Sequelize.literal(
324 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 355 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
325 ) 356 )
326 } 357 }
327 } 358 }
328 } 359 }
329 360
330 const scopes: any[] = [ 361 const scopes: any[] = [
331 ScopeNames.WITH_ACCOUNT, 362 ScopeNames.WITH_ACCOUNT_FOR_API,
332 { 363 {
333 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 364 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
334 } 365 }
335 ] 366 ]
336 367
@@ -367,13 +398,23 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
367 .findAll(query) 398 .findAll(query)
368 } 399 }
369 400
370 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction, order: 'ASC' | 'DESC' = 'ASC') { 401 static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) {
402 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({
403 videoId: video.id,
404 isVideoOwned: video.isOwned()
405 })
406
371 const query = { 407 const query = {
372 order: [ [ 'createdAt', order ] ] as Order, 408 order: [ [ 'createdAt', 'ASC' ] ] as Order,
373 offset: start, 409 offset: start,
374 limit: count, 410 limit: count,
375 where: { 411 where: {
376 videoId 412 videoId: video.id,
413 accountId: {
414 [Op.notIn]: Sequelize.literal(
415 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
416 )
417 }
377 }, 418 },
378 transaction: t 419 transaction: t
379 } 420 }
@@ -392,7 +433,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
392 deletedAt: null, 433 deletedAt: null,
393 accountId: { 434 accountId: {
394 [Op.notIn]: Sequelize.literal( 435 [Op.notIn]: Sequelize.literal(
395 '(' + buildBlockedAccountSQL(serverActor.Account.id) + ')' 436 '(' + buildBlockedAccountSQL([ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]) + ')'
396 ) 437 )
397 } 438 }
398 }, 439 },
@@ -403,7 +444,14 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
403 required: true, 444 required: true,
404 where: { 445 where: {
405 privacy: VideoPrivacy.PUBLIC 446 privacy: VideoPrivacy.PUBLIC
406 } 447 },
448 include: [
449 {
450 attributes: [ 'accountId' ],
451 model: VideoChannelModel.unscoped(),
452 required: true
453 }
454 ]
407 } 455 }
408 ] 456 ]
409 } 457 }
@@ -415,6 +463,43 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
415 .findAll(query) 463 .findAll(query)
416 } 464 }
417 465
466 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
467 const accountWhere = filter.onVideosOfAccount
468 ? { id: filter.onVideosOfAccount.id }
469 : {}
470
471 const query = {
472 limit: 1000,
473 where: {
474 deletedAt: null,
475 accountId: ofAccount.id
476 },
477 include: [
478 {
479 model: VideoModel,
480 required: true,
481 include: [
482 {
483 model: VideoChannelModel,
484 required: true,
485 include: [
486 {
487 model: AccountModel,
488 required: true,
489 where: accountWhere
490 }
491 ]
492 }
493 ]
494 }
495 ]
496 }
497
498 return VideoCommentModel
499 .scope([ ScopeNames.WITH_ACCOUNT ])
500 .findAll(query)
501 }
502
418 static async getStats () { 503 static async getStats () {
419 const totalLocalVideoComments = await VideoCommentModel.count({ 504 const totalLocalVideoComments = await VideoCommentModel.count({
420 include: [ 505 include: [
@@ -450,7 +535,9 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
450 videoId, 535 videoId,
451 accountId: { 536 accountId: {
452 [Op.notIn]: buildLocalAccountIdsIn() 537 [Op.notIn]: buildLocalAccountIdsIn()
453 } 538 },
539 // Do not delete Tombstones
540 deletedAt: null
454 } 541 }
455 } 542 }
456 543
@@ -579,4 +666,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
579 tag 666 tag
580 } 667 }
581 } 668 }
669
670 private static async buildBlockerAccountIds (options: {
671 videoId: number
672 isVideoOwned: boolean
673 user?: MUserAccountId
674 }) {
675 const { videoId, user, isVideoOwned } = options
676
677 const serverActor = await getServerActor()
678 const blockerAccountIds = [ serverActor.Account.id ]
679
680 if (user) blockerAccountIds.push(user.Account.id)
681
682 if (isVideoOwned) {
683 const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId)
684 blockerAccountIds.push(videoOwnerAccount.id)
685 }
686
687 return blockerAccountIds
688 }
582} 689}
diff --git a/server/tests/api/check-params/bulk.ts b/server/tests/api/check-params/bulk.ts
new file mode 100644
index 000000000..432858b33
--- /dev/null
+++ b/server/tests/api/check-params/bulk.ts
@@ -0,0 +1,88 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import {
5 cleanupTests,
6 createUser,
7 flushAndRunServer,
8 ServerInfo,
9 setAccessTokensToServers,
10 userLogin
11} from '../../../../shared/extra-utils'
12import { makePostBodyRequest } from '../../../../shared/extra-utils/requests/requests'
13
14describe('Test bulk API validators', function () {
15 let server: ServerInfo
16 let userAccessToken: string
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(120000)
22
23 server = await flushAndRunServer(1)
24 await setAccessTokensToServers([ server ])
25
26 const user = { username: 'user1', password: 'password' }
27 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
28
29 userAccessToken = await userLogin(server, user)
30 })
31
32 describe('When removing comments of', function () {
33 const path = '/api/v1/bulk/remove-comments-of'
34
35 it('Should fail with an unauthenticated user', async function () {
36 await makePostBodyRequest({
37 url: server.url,
38 path,
39 fields: { accountName: 'user1', scope: 'my-videos' },
40 statusCodeExpected: 401
41 })
42 })
43
44 it('Should fail with an unknown account', async function () {
45 await makePostBodyRequest({
46 url: server.url,
47 token: server.accessToken,
48 path,
49 fields: { accountName: 'user2', scope: 'my-videos' },
50 statusCodeExpected: 404
51 })
52 })
53
54 it('Should fail with an invalid scope', async function () {
55 await makePostBodyRequest({
56 url: server.url,
57 token: server.accessToken,
58 path,
59 fields: { accountName: 'user1', scope: 'my-videoss' },
60 statusCodeExpected: 400
61 })
62 })
63
64 it('Should fail to delete comments of the instance without the appropriate rights', async function () {
65 await makePostBodyRequest({
66 url: server.url,
67 token: userAccessToken,
68 path,
69 fields: { accountName: 'user1', scope: 'instance' },
70 statusCodeExpected: 403
71 })
72 })
73
74 it('Should succeed with the correct params', async function () {
75 await makePostBodyRequest({
76 url: server.url,
77 token: server.accessToken,
78 path,
79 fields: { accountName: 'user1', scope: 'instance' },
80 statusCodeExpected: 204
81 })
82 })
83 })
84
85 after(async function () {
86 await cleanupTests([ server ])
87 })
88})
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index f1a79806b..7c96fa762 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -133,6 +133,12 @@ describe('Test config API validators', function () {
133 indexUrl: 'https://index.example.com' 133 indexUrl: 'https://index.example.com'
134 } 134 }
135 } 135 }
136 },
137 broadcastMessage: {
138 enabled: true,
139 dismissable: true,
140 message: 'super message',
141 level: 'warning'
136 } 142 }
137 } 143 }
138 144
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index ef152f55c..93ffd98b1 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,5 +1,6 @@
1import './accounts' 1import './accounts'
2import './blocklist' 2import './blocklist'
3import './bulk'
3import './config' 4import './config'
4import './contact-form' 5import './contact-form'
5import './debug' 6import './debug'
diff --git a/server/tests/api/server/bulk.ts b/server/tests/api/server/bulk.ts
new file mode 100644
index 000000000..63321d4bb
--- /dev/null
+++ b/server/tests/api/server/bulk.ts
@@ -0,0 +1,198 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { VideoComment } from '@shared/models/videos/video-comment.model'
6import {
7 addVideoCommentThread,
8 bulkRemoveCommentsOf,
9 cleanupTests,
10 createUser,
11 flushAndRunMultipleServers,
12 getVideoCommentThreads,
13 getVideosList,
14 ServerInfo,
15 setAccessTokensToServers,
16 uploadVideo,
17 userLogin,
18 waitJobs,
19 addVideoCommentReply
20} from '../../../../shared/extra-utils/index'
21import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
22import { Video } from '@shared/models'
23
24const expect = chai.expect
25
26describe('Test bulk actions', function () {
27 const commentsUser3: { videoId: number, commentId: number }[] = []
28
29 let servers: ServerInfo[] = []
30 let user1AccessToken: string
31 let user2AccessToken: string
32 let user3AccessToken: string
33
34 before(async function () {
35 this.timeout(30000)
36
37 servers = await flushAndRunMultipleServers(2)
38
39 // Get the access tokens
40 await setAccessTokensToServers(servers)
41
42 {
43 const user = { username: 'user1', password: 'password' }
44 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
45
46 user1AccessToken = await userLogin(servers[0], user)
47 }
48
49 {
50 const user = { username: 'user2', password: 'password' }
51 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
52
53 user2AccessToken = await userLogin(servers[0], user)
54 }
55
56 {
57 const user = { username: 'user3', password: 'password' }
58 await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
59
60 user3AccessToken = await userLogin(servers[1], user)
61 }
62
63 await doubleFollow(servers[0], servers[1])
64 })
65
66 describe('Bulk remove comments', function () {
67 async function checkInstanceCommentsRemoved () {
68 {
69 const res = await getVideosList(servers[0].url)
70 const videos = res.body.data as Video[]
71
72 // Server 1 should not have these comments anymore
73 for (const video of videos) {
74 const resThreads = await getVideoCommentThreads(servers[0].url, video.id, 0, 10)
75 const comments = resThreads.body.data as VideoComment[]
76 const comment = comments.find(c => c.text === 'comment by user 3')
77
78 expect(comment).to.not.exist
79 }
80 }
81
82 {
83 const res = await getVideosList(servers[1].url)
84 const videos = res.body.data as Video[]
85
86 // Server 1 should not have these comments on videos of server 1
87 for (const video of videos) {
88 const resThreads = await getVideoCommentThreads(servers[1].url, video.id, 0, 10)
89 const comments = resThreads.body.data as VideoComment[]
90 const comment = comments.find(c => c.text === 'comment by user 3')
91
92 if (video.account.host === 'localhost:' + servers[0].port) {
93 expect(comment).to.not.exist
94 } else {
95 expect(comment).to.exist
96 }
97 }
98 }
99 }
100
101 before(async function () {
102 this.timeout(60000)
103
104 await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1 server 1' })
105 await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' })
106 await uploadVideo(servers[0].url, user1AccessToken, { name: 'video 3 server 1' })
107
108 await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
109
110 await waitJobs(servers)
111
112 {
113 const res = await getVideosList(servers[0].url)
114 for (const video of res.body.data) {
115 await addVideoCommentThread(servers[0].url, servers[0].accessToken, video.id, 'comment by root server 1')
116 await addVideoCommentThread(servers[0].url, user1AccessToken, video.id, 'comment by user 1')
117 await addVideoCommentThread(servers[0].url, user2AccessToken, video.id, 'comment by user 2')
118 }
119 }
120
121 {
122 const res = await getVideosList(servers[1].url)
123 for (const video of res.body.data) {
124 await addVideoCommentThread(servers[1].url, servers[1].accessToken, video.id, 'comment by root server 2')
125
126 const res = await addVideoCommentThread(servers[1].url, user3AccessToken, video.id, 'comment by user 3')
127 commentsUser3.push({ videoId: video.id, commentId: res.body.comment.id })
128 }
129 }
130
131 await waitJobs(servers)
132 })
133
134 it('Should delete comments of an account on my videos', async function () {
135 this.timeout(60000)
136
137 await bulkRemoveCommentsOf({
138 url: servers[0].url,
139 token: user1AccessToken,
140 attributes: {
141 accountName: 'user2',
142 scope: 'my-videos'
143 }
144 })
145
146 await waitJobs(servers)
147
148 for (const server of servers) {
149 const res = await getVideosList(server.url)
150
151 for (const video of res.body.data) {
152 const resThreads = await getVideoCommentThreads(server.url, video.id, 0, 10)
153 const comments = resThreads.body.data as VideoComment[]
154 const comment = comments.find(c => c.text === 'comment by user 2')
155
156 if (video.name === 'video 3 server 1') {
157 expect(comment).to.not.exist
158 } else {
159 expect(comment).to.exist
160 }
161 }
162 }
163 })
164
165 it('Should delete comments of an account on the instance', async function () {
166 this.timeout(60000)
167
168 await bulkRemoveCommentsOf({
169 url: servers[0].url,
170 token: servers[0].accessToken,
171 attributes: {
172 accountName: 'user3@localhost:' + servers[1].port,
173 scope: 'instance'
174 }
175 })
176
177 await waitJobs(servers)
178
179 await checkInstanceCommentsRemoved()
180 })
181
182 it('Should not re create the comment on video update', async function () {
183 this.timeout(60000)
184
185 for (const obj of commentsUser3) {
186 await addVideoCommentReply(servers[1].url, user3AccessToken, obj.videoId, obj.commentId, 'comment by user 3 bis')
187 }
188
189 await waitJobs(servers)
190
191 await checkInstanceCommentsRemoved()
192 })
193 })
194
195 after(async function () {
196 await cleanupTests(servers)
197 })
198})
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 8580835d6..d18a93082 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -87,6 +87,11 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
87 expect(data.followings.instance.autoFollowBack.enabled).to.be.false 87 expect(data.followings.instance.autoFollowBack.enabled).to.be.false
88 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false 88 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
89 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('') 89 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('')
90
91 expect(data.broadcastMessage.enabled).to.be.false
92 expect(data.broadcastMessage.level).to.equal('info')
93 expect(data.broadcastMessage.message).to.equal('')
94 expect(data.broadcastMessage.dismissable).to.be.false
90} 95}
91 96
92function checkUpdatedConfig (data: CustomConfig) { 97function checkUpdatedConfig (data: CustomConfig) {
@@ -155,6 +160,11 @@ function checkUpdatedConfig (data: CustomConfig) {
155 expect(data.followings.instance.autoFollowBack.enabled).to.be.true 160 expect(data.followings.instance.autoFollowBack.enabled).to.be.true
156 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true 161 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
157 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com') 162 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
163
164 expect(data.broadcastMessage.enabled).to.be.true
165 expect(data.broadcastMessage.level).to.equal('error')
166 expect(data.broadcastMessage.message).to.equal('super bad message')
167 expect(data.broadcastMessage.dismissable).to.be.true
158} 168}
159 169
160describe('Test config', function () { 170describe('Test config', function () {
@@ -324,6 +334,12 @@ describe('Test config', function () {
324 indexUrl: 'https://updated.example.com' 334 indexUrl: 'https://updated.example.com'
325 } 335 }
326 } 336 }
337 },
338 broadcastMessage: {
339 enabled: true,
340 level: 'error',
341 message: 'super bad message',
342 dismissable: true
327 } 343 }
328 } 344 }
329 await updateCustomConfig(server.url, server.accessToken, newCustomConfig) 345 await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
index 21b9ae4f8..8c9107a50 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/users/blocklist.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' 5import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser, 8 createUser,
@@ -11,7 +11,9 @@ import {
11 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
12 ServerInfo, 12 ServerInfo,
13 uploadVideo, 13 uploadVideo,
14 userLogin 14 userLogin,
15 follow,
16 unfollow
15} from '../../../../shared/extra-utils/index' 17} from '../../../../shared/extra-utils/index'
16import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 18import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
17import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos' 19import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos'
@@ -19,7 +21,8 @@ import {
19 addVideoCommentReply, 21 addVideoCommentReply,
20 addVideoCommentThread, 22 addVideoCommentThread,
21 getVideoCommentThreads, 23 getVideoCommentThreads,
22 getVideoThreadComments 24 getVideoThreadComments,
25 findCommentId
23} from '../../../../shared/extra-utils/videos/video-comments' 26} from '../../../../shared/extra-utils/videos/video-comments'
24import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 27import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 28import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
@@ -45,13 +48,13 @@ async function checkAllVideos (url: string, token: string) {
45 { 48 {
46 const res = await getVideosListWithToken(url, token) 49 const res = await getVideosListWithToken(url, token)
47 50
48 expect(res.body.data).to.have.lengthOf(4) 51 expect(res.body.data).to.have.lengthOf(5)
49 } 52 }
50 53
51 { 54 {
52 const res = await getVideosList(url) 55 const res = await getVideosList(url)
53 56
54 expect(res.body.data).to.have.lengthOf(4) 57 expect(res.body.data).to.have.lengthOf(5)
55 } 58 }
56} 59}
57 60
@@ -76,13 +79,15 @@ async function checkCommentNotification (
76 check: 'presence' | 'absence' 79 check: 'presence' | 'absence'
77) { 80) {
78 const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text) 81 const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text)
79 const threadId = resComment.body.comment.id 82 const created = resComment.body.comment as VideoComment
83 const threadId = created.id
84 const createdAt = created.createdAt
80 85
81 await waitJobs([ mainServer, comment.server ]) 86 await waitJobs([ mainServer, comment.server ])
82 87
83 const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30) 88 const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30)
84 const commentNotifications = res.body.data 89 const commentNotifications = (res.body.data as UserNotification[])
85 .filter(n => n.comment && n.comment.id === threadId) 90 .filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt)
86 91
87 if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1) 92 if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1)
88 else expect(commentNotifications).to.have.lengthOf(0) 93 else expect(commentNotifications).to.have.lengthOf(0)
@@ -96,6 +101,7 @@ describe('Test blocklist', function () {
96 let servers: ServerInfo[] 101 let servers: ServerInfo[]
97 let videoUUID1: string 102 let videoUUID1: string
98 let videoUUID2: string 103 let videoUUID2: string
104 let videoUUID3: string
99 let userToken1: string 105 let userToken1: string
100 let userModeratorToken: string 106 let userModeratorToken: string
101 let userToken2: string 107 let userToken2: string
@@ -103,7 +109,7 @@ describe('Test blocklist', function () {
103 before(async function () { 109 before(async function () {
104 this.timeout(60000) 110 this.timeout(60000)
105 111
106 servers = await flushAndRunMultipleServers(2) 112 servers = await flushAndRunMultipleServers(3)
107 await setAccessTokensToServers(servers) 113 await setAccessTokensToServers(servers)
108 114
109 { 115 {
@@ -139,7 +145,13 @@ describe('Test blocklist', function () {
139 videoUUID2 = res.body.video.uuid 145 videoUUID2 = res.body.video.uuid
140 } 146 }
141 147
148 {
149 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' })
150 videoUUID3 = res.body.video.uuid
151 }
152
142 await doubleFollow(servers[0], servers[1]) 153 await doubleFollow(servers[0], servers[1])
154 await doubleFollow(servers[0], servers[2])
143 155
144 { 156 {
145 const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1') 157 const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1')
@@ -174,7 +186,7 @@ describe('Test blocklist', function () {
174 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) 186 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
175 187
176 const videos: Video[] = res.body.data 188 const videos: Video[] = res.body.data
177 expect(videos).to.have.lengthOf(3) 189 expect(videos).to.have.lengthOf(4)
178 190
179 const v = videos.find(v => v.name === 'video user 2') 191 const v = videos.find(v => v.name === 'video user 2')
180 expect(v).to.be.undefined 192 expect(v).to.be.undefined
@@ -188,14 +200,14 @@ describe('Test blocklist', function () {
188 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) 200 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
189 201
190 const videos: Video[] = res.body.data 202 const videos: Video[] = res.body.data
191 expect(videos).to.have.lengthOf(2) 203 expect(videos).to.have.lengthOf(3)
192 204
193 const v = videos.find(v => v.name === 'video user 1') 205 const v = videos.find(v => v.name === 'video user 1')
194 expect(v).to.be.undefined 206 expect(v).to.be.undefined
195 }) 207 })
196 208
197 it('Should hide its comments', async function () { 209 it('Should hide its comments', async function () {
198 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', servers[0].accessToken) 210 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 25, '-createdAt', servers[0].accessToken)
199 211
200 const threads: VideoComment[] = resThreads.body.data 212 const threads: VideoComment[] = resThreads.body.data
201 expect(threads).to.have.lengthOf(1) 213 expect(threads).to.have.lengthOf(1)
@@ -235,10 +247,6 @@ describe('Test blocklist', function () {
235 return checkAllVideos(servers[0].url, userToken1) 247 return checkAllVideos(servers[0].url, userToken1)
236 }) 248 })
237 249
238 it('Should list all the comments with another user', async function () {
239 return checkAllComments(servers[0].url, userToken1, videoUUID1)
240 })
241
242 it('Should list blocked accounts', async function () { 250 it('Should list blocked accounts', async function () {
243 { 251 {
244 const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt') 252 const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
@@ -269,6 +277,61 @@ describe('Test blocklist', function () {
269 } 277 }
270 }) 278 })
271 279
280 it('Should not allow a remote blocked user to comment my videos', async function () {
281 this.timeout(60000)
282
283 {
284 await addVideoCommentThread(servers[1].url, userToken2, videoUUID3, 'comment user 2')
285 await waitJobs(servers)
286
287 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID3, 'uploader')
288 await waitJobs(servers)
289
290 const commentId = await findCommentId(servers[1].url, videoUUID3, 'uploader')
291 const message = 'reply by user 2'
292 const resReply = await addVideoCommentReply(servers[1].url, userToken2, videoUUID3, commentId, message)
293 await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID3, resReply.body.comment.id, 'another reply')
294
295 await waitJobs(servers)
296 }
297
298 // Server 2 has all the comments
299 {
300 const resThreads = await getVideoCommentThreads(servers[1].url, videoUUID3, 0, 25, '-createdAt')
301 const threads: VideoComment[] = resThreads.body.data
302
303 expect(threads).to.have.lengthOf(2)
304 expect(threads[0].text).to.equal('uploader')
305 expect(threads[1].text).to.equal('comment user 2')
306
307 const resReplies = await getVideoThreadComments(servers[1].url, videoUUID3, threads[0].id)
308
309 const tree: VideoCommentThreadTree = resReplies.body
310 expect(tree.children).to.have.lengthOf(1)
311 expect(tree.children[0].comment.text).to.equal('reply by user 2')
312 expect(tree.children[0].children).to.have.lengthOf(1)
313 expect(tree.children[0].children[0].comment.text).to.equal('another reply')
314 }
315
316 // Server 1 and 3 should only have uploader comments
317 for (const server of [ servers[0], servers[2] ]) {
318 const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt')
319 const threads: VideoComment[] = resThreads.body.data
320
321 expect(threads).to.have.lengthOf(1)
322 expect(threads[0].text).to.equal('uploader')
323
324 const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id)
325
326 const tree: VideoCommentThreadTree = resReplies.body
327 if (server.serverNumber === 1) {
328 expect(tree.children).to.have.lengthOf(0)
329 } else {
330 expect(tree.children).to.have.lengthOf(1)
331 }
332 }
333 })
334
272 it('Should unblock the remote account', async function () { 335 it('Should unblock the remote account', async function () {
273 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port) 336 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
274 }) 337 })
@@ -277,12 +340,37 @@ describe('Test blocklist', function () {
277 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) 340 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
278 341
279 const videos: Video[] = res.body.data 342 const videos: Video[] = res.body.data
280 expect(videos).to.have.lengthOf(3) 343 expect(videos).to.have.lengthOf(4)
281 344
282 const v = videos.find(v => v.name === 'video user 2') 345 const v = videos.find(v => v.name === 'video user 2')
283 expect(v).not.to.be.undefined 346 expect(v).not.to.be.undefined
284 }) 347 })
285 348
349 it('Should display its comments on my video', async function () {
350 for (const server of servers) {
351 const resThreads = await getVideoCommentThreads(server.url, videoUUID3, 0, 25, '-createdAt')
352 const threads: VideoComment[] = resThreads.body.data
353
354 // Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment
355 if (server.serverNumber === 3) {
356 expect(threads).to.have.lengthOf(1)
357 continue
358 }
359
360 expect(threads).to.have.lengthOf(2)
361 expect(threads[0].text).to.equal('uploader')
362 expect(threads[1].text).to.equal('comment user 2')
363
364 const resReplies = await getVideoThreadComments(server.url, videoUUID3, threads[0].id)
365
366 const tree: VideoCommentThreadTree = resReplies.body
367 expect(tree.children).to.have.lengthOf(1)
368 expect(tree.children[0].comment.text).to.equal('reply by user 2')
369 expect(tree.children[0].children).to.have.lengthOf(1)
370 expect(tree.children[0].children[0].comment.text).to.equal('another reply')
371 }
372 })
373
286 it('Should unblock the local account', async function () { 374 it('Should unblock the local account', async function () {
287 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1') 375 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
288 }) 376 })
@@ -328,7 +416,7 @@ describe('Test blocklist', function () {
328 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) 416 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
329 417
330 const videos: Video[] = res.body.data 418 const videos: Video[] = res.body.data
331 expect(videos).to.have.lengthOf(2) 419 expect(videos).to.have.lengthOf(3)
332 420
333 const v1 = videos.find(v => v.name === 'video user 2') 421 const v1 = videos.find(v => v.name === 'video user 2')
334 const v2 = videos.find(v => v.name === 'video server 2') 422 const v2 = videos.find(v => v.name === 'video server 2')
@@ -442,7 +530,7 @@ describe('Test blocklist', function () {
442 const res = await getVideosListWithToken(servers[0].url, token) 530 const res = await getVideosListWithToken(servers[0].url, token)
443 531
444 const videos: Video[] = res.body.data 532 const videos: Video[] = res.body.data
445 expect(videos).to.have.lengthOf(3) 533 expect(videos).to.have.lengthOf(4)
446 534
447 const v = videos.find(v => v.name === 'video user 2') 535 const v = videos.find(v => v.name === 'video user 2')
448 expect(v).to.be.undefined 536 expect(v).to.be.undefined
@@ -458,7 +546,7 @@ describe('Test blocklist', function () {
458 const res = await getVideosListWithToken(servers[0].url, token) 546 const res = await getVideosListWithToken(servers[0].url, token)
459 547
460 const videos: Video[] = res.body.data 548 const videos: Video[] = res.body.data
461 expect(videos).to.have.lengthOf(2) 549 expect(videos).to.have.lengthOf(3)
462 550
463 const v = videos.find(v => v.name === 'video user 1') 551 const v = videos.find(v => v.name === 'video user 1')
464 expect(v).to.be.undefined 552 expect(v).to.be.undefined
@@ -467,9 +555,11 @@ describe('Test blocklist', function () {
467 555
468 it('Should hide its comments', async function () { 556 it('Should hide its comments', async function () {
469 for (const token of [ userModeratorToken, servers[0].accessToken ]) { 557 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
470 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', token) 558 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 20, '-createdAt', token)
559
560 let threads: VideoComment[] = resThreads.body.data
561 threads = threads.filter(t => t.isDeleted === false)
471 562
472 const threads: VideoComment[] = resThreads.body.data
473 expect(threads).to.have.lengthOf(1) 563 expect(threads).to.have.lengthOf(1)
474 expect(threads[0].totalReplies).to.equal(0) 564 expect(threads[0].totalReplies).to.equal(0)
475 565
@@ -543,7 +633,7 @@ describe('Test blocklist', function () {
543 const res = await getVideosListWithToken(servers[0].url, token) 633 const res = await getVideosListWithToken(servers[0].url, token)
544 634
545 const videos: Video[] = res.body.data 635 const videos: Video[] = res.body.data
546 expect(videos).to.have.lengthOf(3) 636 expect(videos).to.have.lengthOf(4)
547 637
548 const v = videos.find(v => v.name === 'video user 2') 638 const v = videos.find(v => v.name === 'video user 2')
549 expect(v).not.to.be.undefined 639 expect(v).not.to.be.undefined
@@ -604,7 +694,7 @@ describe('Test blocklist', function () {
604 694
605 for (const res of [ res1, res2 ]) { 695 for (const res of [ res1, res2 ]) {
606 const videos: Video[] = res.body.data 696 const videos: Video[] = res.body.data
607 expect(videos).to.have.lengthOf(2) 697 expect(videos).to.have.lengthOf(3)
608 698
609 const v1 = videos.find(v => v.name === 'video user 2') 699 const v1 = videos.find(v => v.name === 'video user 2')
610 const v2 = videos.find(v => v.name === 'video server 2') 700 const v2 = videos.find(v => v.name === 'video server 2')
@@ -629,7 +719,7 @@ describe('Test blocklist', function () {
629 }) 719 })
630 720
631 it('Should not have notification from blocked instances by instance', async function () { 721 it('Should not have notification from blocked instances by instance', async function () {
632 this.timeout(20000) 722 this.timeout(50000)
633 723
634 { 724 {
635 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } 725 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
@@ -645,6 +735,24 @@ describe('Test blocklist', function () {
645 } 735 }
646 await checkCommentNotification(servers[0], comment, 'absence') 736 await checkCommentNotification(servers[0], comment, 'absence')
647 } 737 }
738
739 {
740 const now = new Date()
741 await unfollow(servers[1].url, servers[1].accessToken, servers[0])
742 await waitJobs(servers)
743 await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken)
744
745 await waitJobs(servers)
746
747 const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30)
748 const commentNotifications = (res.body.data as UserNotification[])
749 .filter(n => {
750 return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER &&
751 n.createdAt >= now.toISOString()
752 })
753
754 expect(commentNotifications).to.have.lengthOf(0)
755 }
648 }) 756 })
649 757
650 it('Should list blocked servers', async function () { 758 it('Should list blocked servers', async function () {
@@ -676,7 +784,7 @@ describe('Test blocklist', function () {
676 }) 784 })
677 785
678 it('Should have notification from unblocked instances', async function () { 786 it('Should have notification from unblocked instances', async function () {
679 this.timeout(20000) 787 this.timeout(50000)
680 788
681 { 789 {
682 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } 790 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
@@ -692,6 +800,24 @@ describe('Test blocklist', function () {
692 } 800 }
693 await checkCommentNotification(servers[0], comment, 'presence') 801 await checkCommentNotification(servers[0], comment, 'presence')
694 } 802 }
803
804 {
805 const now = new Date()
806 await unfollow(servers[1].url, servers[1].accessToken, servers[0])
807 await waitJobs(servers)
808 await follow(servers[1].url, [ servers[0].host ], servers[1].accessToken)
809
810 await waitJobs(servers)
811
812 const res = await getUserNotifications(servers[0].url, servers[0].accessToken, 0, 30)
813 const commentNotifications = (res.body.data as UserNotification[])
814 .filter(n => {
815 return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER &&
816 n.createdAt >= now.toISOString()
817 })
818
819 expect(commentNotifications).to.have.lengthOf(1)
820 }
695 }) 821 })
696 }) 822 })
697 }) 823 })
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index e3029f1ae..d7b04373f 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -37,7 +37,8 @@ import {
37 addVideoCommentThread, 37 addVideoCommentThread,
38 deleteVideoComment, 38 deleteVideoComment,
39 getVideoCommentThreads, 39 getVideoCommentThreads,
40 getVideoThreadComments 40 getVideoThreadComments,
41 findCommentId
41} from '../../../../shared/extra-utils/videos/video-comments' 42} from '../../../../shared/extra-utils/videos/video-comments'
42import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 43import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
43 44
@@ -773,8 +774,7 @@ describe('Test multiple servers', function () {
773 await waitJobs(servers) 774 await waitJobs(servers)
774 775
775 { 776 {
776 const res = await getVideoCommentThreads(servers[1].url, videoUUID, 0, 5) 777 const threadId = await findCommentId(servers[1].url, videoUUID, 'my super first comment')
777 const threadId = res.body.data.find(c => c.text === 'my super first comment').id
778 778
779 const text = 'my super answer to thread 1' 779 const text = 'my super answer to thread 1'
780 await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text) 780 await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text)
@@ -783,8 +783,7 @@ describe('Test multiple servers', function () {
783 await waitJobs(servers) 783 await waitJobs(servers)
784 784
785 { 785 {
786 const res1 = await getVideoCommentThreads(servers[2].url, videoUUID, 0, 5) 786 const threadId = await findCommentId(servers[2].url, videoUUID, 'my super first comment')
787 const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
788 787
789 const res2 = await getVideoThreadComments(servers[2].url, videoUUID, threadId) 788 const res2 = await getVideoThreadComments(servers[2].url, videoUUID, threadId)
790 const childCommentId = res2.body.children[0].comment.id 789 const childCommentId = res2.body.children[0].comment.id
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index 7fac921a3..ba961cdba 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -1,7 +1,14 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import * as libxmljs from 'libxmljs'
6import {
7 addAccountToAccountBlocklist,
8 addAccountToServerBlocklist,
9 removeAccountFromServerBlocklist
10} from '@shared/extra-utils/users/blocklist'
11import { VideoPrivacy } from '@shared/models'
5import { 12import {
6 cleanupTests, 13 cleanupTests,
7 createUser, 14 createUser,
@@ -13,14 +20,12 @@ import {
13 ServerInfo, 20 ServerInfo,
14 setAccessTokensToServers, 21 setAccessTokensToServers,
15 uploadVideo, 22 uploadVideo,
23 uploadVideoAndGetId,
16 userLogin 24 userLogin
17} from '../../../shared/extra-utils' 25} from '../../../shared/extra-utils'
18import * as libxmljs from 'libxmljs'
19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
20import { waitJobs } from '../../../shared/extra-utils/server/jobs' 26import { waitJobs } from '../../../shared/extra-utils/server/jobs'
27import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
21import { User } from '../../../shared/models/users' 28import { User } from '../../../shared/models/users'
22import { VideoPrivacy } from '@shared/models'
23import { addAccountToServerBlocklist } from '@shared/extra-utils/users/blocklist'
24 29
25chai.use(require('chai-xml')) 30chai.use(require('chai-xml'))
26chai.use(require('chai-json-schema')) 31chai.use(require('chai-json-schema'))
@@ -219,7 +224,11 @@ describe('Test syndication feeds', () => {
219 }) 224 })
220 225
221 it('Should not list comments from muted accounts or instances', async function () { 226 it('Should not list comments from muted accounts or instances', async function () {
222 await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, 'root@localhost:' + servers[0].port) 227 this.timeout(30000)
228
229 const remoteHandle = 'root@localhost:' + servers[0].port
230
231 await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
223 232
224 { 233 {
225 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 }) 234 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 })
@@ -227,6 +236,26 @@ describe('Test syndication feeds', () => {
227 expect(jsonObj.items.length).to.be.equal(0) 236 expect(jsonObj.items.length).to.be.equal(0)
228 } 237 }
229 238
239 await removeAccountFromServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
240
241 {
242 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })).uuid
243 await waitJobs(servers)
244 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'super comment')
245 await waitJobs(servers)
246
247 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 3 })
248 const jsonObj = JSON.parse(json.text)
249 expect(jsonObj.items.length).to.be.equal(3)
250 }
251
252 await addAccountToAccountBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
253
254 {
255 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 4 })
256 const jsonObj = JSON.parse(json.text)
257 expect(jsonObj.items.length).to.be.equal(2)
258 }
230 }) 259 })
231 }) 260 })
232 261