diff options
Diffstat (limited to 'server')
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { asyncMiddleware, authenticate } from '../../middlewares' | ||
3 | import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk' | ||
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
5 | import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' | ||
6 | import { removeComment } from '@server/lib/video-comment' | ||
7 | |||
8 | const bulkRouter = express.Router() | ||
9 | |||
10 | bulkRouter.post('/remove-comments-of', | ||
11 | authenticate, | ||
12 | asyncMiddleware(bulkRemoveCommentsOfValidator), | ||
13 | asyncMiddleware(bulkRemoveCommentsOf) | ||
14 | ) | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | bulkRouter | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | async 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 @@ | |||
1 | import * as cors from 'cors' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import * as RateLimit from 'express-rate-limit' | ||
4 | import { badRequest } from '../../helpers/express-utils' | ||
5 | import { CONFIG } from '../../initializers/config' | ||
6 | import { accountsRouter } from './accounts' | ||
7 | import { bulkRouter } from './bulk' | ||
2 | import { configRouter } from './config' | 8 | import { configRouter } from './config' |
3 | import { jobsRouter } from './jobs' | 9 | import { jobsRouter } from './jobs' |
4 | import { oauthClientsRouter } from './oauth-clients' | 10 | import { oauthClientsRouter } from './oauth-clients' |
11 | import { overviewsRouter } from './overviews' | ||
12 | import { pluginRouter } from './plugins' | ||
13 | import { searchRouter } from './search' | ||
5 | import { serverRouter } from './server' | 14 | import { serverRouter } from './server' |
6 | import { usersRouter } from './users' | 15 | import { usersRouter } from './users' |
7 | import { accountsRouter } from './accounts' | ||
8 | import { videosRouter } from './videos' | ||
9 | import { badRequest } from '../../helpers/express-utils' | ||
10 | import { videoChannelRouter } from './video-channel' | 16 | import { videoChannelRouter } from './video-channel' |
11 | import * as cors from 'cors' | ||
12 | import { searchRouter } from './search' | ||
13 | import { overviewsRouter } from './overviews' | ||
14 | import { videoPlaylistRouter } from './video-playlist' | 17 | import { videoPlaylistRouter } from './video-playlist' |
15 | import { CONFIG } from '../../initializers/config' | 18 | import { videosRouter } from './videos' |
16 | import { pluginRouter } from './plugins' | ||
17 | import * as RateLimit from 'express-rate-limit' | ||
18 | 19 | ||
19 | const apiRouter = express.Router() | 20 | const apiRouter = express.Router() |
20 | 21 | ||
@@ -31,6 +32,7 @@ const apiRateLimiter = RateLimit({ | |||
31 | apiRouter.use(apiRateLimiter) | 32 | apiRouter.use(apiRateLimiter) |
32 | 33 | ||
33 | apiRouter.use('/server', serverRouter) | 34 | apiRouter.use('/server', serverRouter) |
35 | apiRouter.use('/bulk', bulkRouter) | ||
34 | apiRouter.use('/oauth-clients', oauthClientsRouter) | 36 | apiRouter.use('/oauth-clients', oauthClientsRouter) |
35 | apiRouter.use('/config', configRouter) | 37 | apiRouter.use('/config', configRouter) |
36 | apiRouter.use('/users', usersRouter) | 38 | apiRouter.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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { cloneDeep } from 'lodash' | ||
3 | import { ResultList } from '../../../../shared/models' | 2 | import { ResultList } from '../../../../shared/models' |
4 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' | 3 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' |
5 | import { logger } from '../../../helpers/logger' | 4 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | 5 | import { getFormattedObjects } from '../../../helpers/utils' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 6 | import { sequelizeTypescript } from '../../../initializers/database' |
8 | import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment' | 7 | import { Notifier } from '../../../lib/notifier' |
8 | import { Hooks } from '../../../lib/plugins/hooks' | ||
9 | import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment' | ||
9 | import { | 10 | import { |
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' |
26 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
27 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | ||
28 | import { AccountModel } from '../../../models/account/account' | 27 | import { AccountModel } from '../../../models/account/account' |
29 | import { Notifier } from '../../../lib/notifier' | 28 | import { VideoCommentModel } from '../../../models/video/video-comment' |
30 | import { Hooks } from '../../../lib/plugins/hooks' | ||
31 | import { sendDeleteVideoComment } from '../../../lib/activitypub/send' | ||
32 | 29 | ||
33 | const auditLogger = auditLoggerFactory('comments') | 30 | const auditLogger = auditLoggerFactory('comments') |
34 | const videoCommentRouter = express.Router() | 31 | const 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 | ||
157 | async function addVideoCommentReply (req: express.Request, res: express.Response) { | 154 | async 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 | ||
179 | async function removeVideoComment (req: express.Request, res: express.Response) { | 176 | async 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 @@ | |||
1 | function isBulkRemoveCommentsOfScopeValid (value: string) { | ||
2 | return value === 'my-videos' || value === 'instance' | ||
3 | } | ||
4 | |||
5 | // --------------------------------------------------------------------------- | ||
6 | |||
7 | export { | ||
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- | |||
6 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 6 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
7 | import * as bytes from 'bytes' | 7 | import * as bytes from 'bytes' |
8 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | 8 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
9 | import { 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 |
11 | let config: IConfig = require('config') | 12 | let 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 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
1 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' | 2 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 4 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | 7 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { resolveThread } from '../video-comments' | ||
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
8 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
9 | import { createOrUpdateCacheFile } from '../cache-file' | ||
10 | import { Notifier } from '../../notifier' | ||
11 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
13 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 8 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
14 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' | 9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' |
15 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 10 | import { Notifier } from '../../notifier' |
11 | import { createOrUpdateCacheFile } from '../cache-file' | ||
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
13 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
14 | import { resolveThread } from '../video-comments' | ||
15 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
16 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
16 | 17 | ||
17 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 18 | async 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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | ||
2 | import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' | 3 | import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' |
4 | import { logger } from '../../../helpers/logger' | ||
3 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
5 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { VideoShareModel } from '../../../models/video/video-share' |
8 | import { MActorUrl } from '../../../typings/models' | ||
9 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' | ||
10 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' | ||
6 | import { getDeleteActivityPubUrl } from '../url' | 11 | import { getDeleteActivityPubUrl } from '../url' |
7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 12 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
8 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' | ||
9 | import { logger } from '../../../helpers/logger' | ||
10 | import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' | ||
11 | import { MActorUrl } from '../../../typings/models' | ||
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 13 | ||
14 | async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { | 14 | async 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 | ||
45 | async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t: Transaction) { | 45 | async 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 @@ | |||
1 | import { sequelizeTypescript } from '@server/initializers/database' | 1 | import { sequelizeTypescript } from '@server/initializers/database' |
2 | import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { MAccountBlocklist, MAccountId, MAccountServer, MServerBlocklist } from '@server/typings/models' | ||
3 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 4 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
4 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 5 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
5 | 6 | ||
@@ -33,9 +34,29 @@ function removeServerFromBlocklist (serverBlock: MServerBlocklist) { | |||
33 | }) | 34 | }) |
34 | } | 35 | } |
35 | 36 | ||
37 | async 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 | |||
36 | export { | 56 | export { |
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 @@ | |||
1 | import { getServerActor } from '@server/models/application/application' | ||
2 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
3 | import { | ||
4 | MUser, | ||
5 | MUserAccount, | ||
6 | MUserDefault, | ||
7 | MUserNotifSettingAccount, | ||
8 | MUserWithNotificationSetting, | ||
9 | UserNotificationModelForApi | ||
10 | } from '@server/typings/models/user' | ||
11 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | ||
1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | 12 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' |
13 | import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' | ||
2 | import { logger } from '../helpers/logger' | 14 | import { logger } from '../helpers/logger' |
3 | import { Emailer } from './emailer' | ||
4 | import { UserNotificationModel } from '../models/account/user-notification' | ||
5 | import { UserModel } from '../models/account/user' | ||
6 | import { PeerTubeSocket } from './peertube-socket' | ||
7 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
8 | import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos' | ||
9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 16 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
17 | import { UserModel } from '../models/account/user' | ||
18 | import { UserNotificationModel } from '../models/account/user-notification' | ||
19 | import { MAccountServer, MActorFollowFull } from '../typings/models' | ||
10 | import { | 20 | import { |
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' |
18 | import { | 28 | import { isBlockedByServerOrAccount } from './blocklist' |
19 | MUser, | 29 | import { Emailer } from './emailer' |
20 | MUserAccount, | 30 | import { PeerTubeSocket } from './peertube-socket' |
21 | MUserDefault, | ||
22 | MUserNotifSettingAccount, | ||
23 | MUserWithNotificationSetting, | ||
24 | UserNotificationModelForApi | ||
25 | } from '@server/typings/models/user' | ||
26 | import { MAccountDefault, MActorFollowFull } from '../typings/models' | ||
27 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | ||
28 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
29 | import { getServerActor } from '@server/models/application/application' | ||
30 | 31 | ||
31 | class Notifier { | 32 | class 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 @@ | |||
1 | import { cloneDeep } from 'lodash' | ||
1 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { sequelizeTypescript } from '@server/initializers/database' | ||
2 | import { ResultList } from '../../shared/models' | 5 | import { ResultList } from '../../shared/models' |
3 | import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' | 6 | import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' |
4 | import { VideoCommentModel } from '../models/video/video-comment' | 7 | import { VideoCommentModel } from '../models/video/video-comment' |
8 | import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight, MCommentOwnerVideo } from '../typings/models' | ||
9 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' | ||
5 | import { getVideoCommentActivityPubUrl } from './activitypub/url' | 10 | import { getVideoCommentActivityPubUrl } from './activitypub/url' |
6 | import { sendCreateVideoComment } from './activitypub/send' | 11 | import { Hooks } from './plugins/hooks' |
7 | import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' | 12 | |
13 | async 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 | ||
9 | async function createVideoComment (obj: { | 31 | async 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 | ||
76 | function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void { | 98 | function 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 | ||
84 | export { | 106 | export { |
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk' | ||
4 | import { doesAccountNameWithHostExist } from '@server/helpers/middlewares' | ||
5 | import { UserRight } from '@shared/models' | ||
6 | import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' | ||
7 | import { logger } from '../../helpers/logger' | ||
8 | import { areValidationErrors } from './utils' | ||
9 | |||
10 | const 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 | |||
37 | export { | ||
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 | |||
32 | import { AccountBlocklistModel } from './account-blocklist' | 32 | import { AccountBlocklistModel } from './account-blocklist' |
33 | import { ServerBlocklistModel } from '../server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../server/server-blocklist' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models' | 35 | import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable, MAccount } from '../../typings/models' |
36 | import * as Bluebird from 'bluebird' | 36 | import * as Bluebird from 'bluebird' |
37 | import { ModelCache } from '@server/models/model-cache' | 37 | import { ModelCache } from '@server/models/model-cache' |
38 | import { VideoModel } from '../video/video' | ||
38 | 39 | ||
39 | export enum ScopeNames { | 40 | export 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 | ||
139 | function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) { | 139 | function 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { uniq } from 'lodash' | ||
3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { MAccount, MAccountId, MUserAccountId } from '@server/typings/models' | ||
7 | import { VideoPrivacy } from '@shared/models' | ||
2 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 8 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
3 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 9 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
4 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 10 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
5 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
6 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | ||
7 | import { AccountModel } from '../account/account' | ||
8 | import { ActorModel } from '../activitypub/actor' | ||
9 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' | ||
10 | import { VideoModel } from './video' | ||
11 | import { VideoChannelModel } from './video-channel' | ||
12 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | 11 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' |
12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
13 | import { regexpCapture } from '../../helpers/regexp' | 13 | import { regexpCapture } from '../../helpers/regexp' |
14 | import { uniq } from 'lodash' | 14 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
15 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | ||
16 | import * as Bluebird from 'bluebird' | ||
17 | import { | 15 | import { |
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' |
28 | import { MUserAccountId } from '@server/typings/models' | 27 | import { AccountModel } from '../account/account' |
29 | import { VideoPrivacy } from '@shared/models' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
30 | import { getServerActor } from '@server/models/application/application' | 29 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' |
30 | import { VideoModel } from './video' | ||
31 | import { VideoChannelModel } from './video-channel' | ||
31 | 32 | ||
32 | enum ScopeNames { | 33 | enum 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 | |||
3 | import 'mocha' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createUser, | ||
7 | flushAndRunServer, | ||
8 | ServerInfo, | ||
9 | setAccessTokensToServers, | ||
10 | userLogin | ||
11 | } from '../../../../shared/extra-utils' | ||
12 | import { makePostBodyRequest } from '../../../../shared/extra-utils/requests/requests' | ||
13 | |||
14 | describe('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 @@ | |||
1 | import './accounts' | 1 | import './accounts' |
2 | import './blocklist' | 2 | import './blocklist' |
3 | import './bulk' | ||
3 | import './config' | 4 | import './config' |
4 | import './contact-form' | 5 | import './contact-form' |
5 | import './debug' | 6 | import './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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { VideoComment } from '@shared/models/videos/video-comment.model' | ||
6 | import { | ||
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' | ||
21 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' | ||
22 | import { Video } from '@shared/models' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | |||
26 | describe('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 | ||
92 | function checkUpdatedConfig (data: CustomConfig) { | 97 | function 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 | ||
160 | describe('Test config', function () { | 170 | describe('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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' | 5 | import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index' |
6 | import { | 6 | import { |
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' |
16 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 18 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
17 | import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos' | 19 | import { 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' |
24 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 27 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
25 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 28 | import { 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' |
42 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 43 | import { 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 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import * as libxmljs from 'libxmljs' | ||
6 | import { | ||
7 | addAccountToAccountBlocklist, | ||
8 | addAccountToServerBlocklist, | ||
9 | removeAccountFromServerBlocklist | ||
10 | } from '@shared/extra-utils/users/blocklist' | ||
11 | import { VideoPrivacy } from '@shared/models' | ||
5 | import { | 12 | import { |
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' |
18 | import * as libxmljs from 'libxmljs' | ||
19 | import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' | ||
20 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | 26 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' |
27 | import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' | ||
21 | import { User } from '../../../shared/models/users' | 28 | import { User } from '../../../shared/models/users' |
22 | import { VideoPrivacy } from '@shared/models' | ||
23 | import { addAccountToServerBlocklist } from '@shared/extra-utils/users/blocklist' | ||
24 | 29 | ||
25 | chai.use(require('chai-xml')) | 30 | chai.use(require('chai-xml')) |
26 | chai.use(require('chai-json-schema')) | 31 | chai.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 | ||