aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/abuse.ts168
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/users/my-history.ts2
-rw-r--r--server/controllers/api/users/my-notifications.ts2
-rw-r--r--server/controllers/api/videos/abuse.ts109
-rw-r--r--server/helpers/audit-logger.ts27
-rw-r--r--server/helpers/custom-validators/abuses.ts61
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts4
-rw-r--r--server/helpers/custom-validators/video-abuses.ts56
-rw-r--r--server/helpers/custom-validators/video-comments.ts81
-rw-r--r--server/helpers/middlewares/abuses.ts47
-rw-r--r--server/helpers/middlewares/accounts.ts4
-rw-r--r--server/helpers/middlewares/index.ts2
-rw-r--r--server/helpers/middlewares/video-abuses.ts32
-rw-r--r--server/initializers/constants.ts29
-rw-r--r--server/initializers/database.ts41
-rw-r--r--server/initializers/migrations/0250-video-abuse-state.ts4
-rw-r--r--server/initializers/migrations/0470-cleanup-indexes.ts (renamed from server/initializers/migrations/0470-cleaup-indexes.ts)0
-rw-r--r--server/initializers/migrations/0520-abuses-split.ts90
-rw-r--r--server/lib/activitypub/process/process-flag.ts117
-rw-r--r--server/lib/activitypub/send/send-flag.ts31
-rw-r--r--server/lib/activitypub/url.ts10
-rw-r--r--server/lib/emailer.ts114
-rw-r--r--server/lib/emails/account-abuse-new/html.pug14
-rw-r--r--server/lib/emails/common/mixins.pug6
-rw-r--r--server/lib/emails/video-abuse-new/html.pug8
-rw-r--r--server/lib/emails/video-comment-abuse-new/html.pug16
-rw-r--r--server/lib/moderation.ts164
-rw-r--r--server/lib/notifier.ts49
-rw-r--r--server/lib/user.ts2
-rw-r--r--server/middlewares/validators/abuse.ts277
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/sort.ts6
-rw-r--r--server/middlewares/validators/user-notifications.ts4
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts135
-rw-r--r--server/middlewares/validators/videos/video-comments.ts70
-rw-r--r--server/models/abuse/abuse-query-builder.ts154
-rw-r--r--server/models/abuse/abuse.ts515
-rw-r--r--server/models/abuse/video-abuse.ts63
-rw-r--r--server/models/abuse/video-comment-abuse.ts47
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts9
-rw-r--r--server/models/account/user-notification-setting.ts8
-rw-r--r--server/models/account/user-notification.ts102
-rw-r--r--server/models/account/user.ts38
-rw-r--r--server/models/server/server-blocklist.ts10
-rw-r--r--server/models/video/video-abuse.ts479
-rw-r--r--server/models/video/video-channel.ts3
-rw-r--r--server/models/video/video-comment.ts27
-rw-r--r--server/models/video/video-query-builder.ts4
-rw-r--r--server/models/video/video.ts88
-rw-r--r--server/tests/api/check-params/abuses.ts269
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/user-notifications.ts2
-rw-r--r--server/tests/api/check-params/video-abuses.ts15
-rw-r--r--server/tests/api/ci-4.sh1
-rw-r--r--server/tests/api/index.ts1
-rw-r--r--server/tests/api/moderation/abuses.ts777
-rw-r--r--server/tests/api/moderation/blocklist.ts (renamed from server/tests/api/users/blocklist.ts)0
-rw-r--r--server/tests/api/moderation/index.ts2
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts85
-rw-r--r--server/tests/api/server/email.ts14
-rw-r--r--server/tests/api/users/index.ts3
-rw-r--r--server/tests/api/users/users.ts42
-rw-r--r--server/tests/api/videos/video-abuse.ts66
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/moderation/abuse.ts103
-rw-r--r--server/types/models/moderation/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts32
-rw-r--r--server/types/models/video/index.ts1
-rw-r--r--server/types/models/video/video-abuse.ts35
-rw-r--r--server/typings/express/index.d.ts4
73 files changed, 3512 insertions, 1286 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
new file mode 100644
index 000000000..04a0c06e3
--- /dev/null
+++ b/server/controllers/api/abuse.ts
@@ -0,0 +1,168 @@
1import * as express from 'express'
2import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
3import { AbuseModel } from '@server/models/abuse/abuse'
4import { getServerActor } from '@server/models/application/application'
5import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared'
6import { getFormattedObjects } from '../../helpers/utils'
7import { sequelizeTypescript } from '../../initializers/database'
8import {
9 abuseGetValidator,
10 abuseListValidator,
11 abuseReportValidator,
12 abusesSortValidator,
13 abuseUpdateValidator,
14 asyncMiddleware,
15 asyncRetryTransactionMiddleware,
16 authenticate,
17 ensureUserHasRight,
18 paginationValidator,
19 setDefaultPagination,
20 setDefaultSort
21} from '../../middlewares'
22import { AccountModel } from '../../models/account/account'
23
24const abuseRouter = express.Router()
25
26abuseRouter.get('/',
27 authenticate,
28 ensureUserHasRight(UserRight.MANAGE_ABUSES),
29 paginationValidator,
30 abusesSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 abuseListValidator,
34 asyncMiddleware(listAbuses)
35)
36abuseRouter.put('/:id',
37 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_ABUSES),
39 asyncMiddleware(abuseUpdateValidator),
40 asyncRetryTransactionMiddleware(updateAbuse)
41)
42abuseRouter.post('/',
43 authenticate,
44 asyncMiddleware(abuseReportValidator),
45 asyncRetryTransactionMiddleware(reportAbuse)
46)
47abuseRouter.delete('/:id',
48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ABUSES),
50 asyncMiddleware(abuseGetValidator),
51 asyncRetryTransactionMiddleware(deleteAbuse)
52)
53
54// ---------------------------------------------------------------------------
55
56export {
57 abuseRouter,
58
59 // FIXME: deprecated in 2.3. Remove these exports
60 listAbuses,
61 updateAbuse,
62 deleteAbuse,
63 reportAbuse
64}
65
66// ---------------------------------------------------------------------------
67
68async function listAbuses (req: express.Request, res: express.Response) {
69 const user = res.locals.oauth.token.user
70 const serverActor = await getServerActor()
71
72 const resultList = await AbuseModel.listForApi({
73 start: req.query.start,
74 count: req.query.count,
75 sort: req.query.sort,
76 id: req.query.id,
77 filter: req.query.filter,
78 predefinedReason: req.query.predefinedReason,
79 search: req.query.search,
80 state: req.query.state,
81 videoIs: req.query.videoIs,
82 searchReporter: req.query.searchReporter,
83 searchReportee: req.query.searchReportee,
84 searchVideo: req.query.searchVideo,
85 searchVideoChannel: req.query.searchVideoChannel,
86 serverAccountId: serverActor.Account.id,
87 user
88 })
89
90 return res.json(getFormattedObjects(resultList.data, resultList.total))
91}
92
93async function updateAbuse (req: express.Request, res: express.Response) {
94 const abuse = res.locals.abuse
95
96 if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment
97 if (req.body.state !== undefined) abuse.state = req.body.state
98
99 await sequelizeTypescript.transaction(t => {
100 return abuse.save({ transaction: t })
101 })
102
103 // Do not send the delete to other instances, we updated OUR copy of this abuse
104
105 return res.type('json').status(204).end()
106}
107
108async function deleteAbuse (req: express.Request, res: express.Response) {
109 const abuse = res.locals.abuse
110
111 await sequelizeTypescript.transaction(t => {
112 return abuse.destroy({ transaction: t })
113 })
114
115 // Do not send the delete to other instances, we delete OUR copy of this abuse
116
117 return res.type('json').status(204).end()
118}
119
120async function reportAbuse (req: express.Request, res: express.Response) {
121 const videoInstance = res.locals.videoAll
122 const commentInstance = res.locals.videoCommentFull
123 const accountInstance = res.locals.account
124
125 const body: AbuseCreate = req.body
126
127 const { id } = await sequelizeTypescript.transaction(async t => {
128 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
129 const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r])
130
131 const baseAbuse = {
132 reporterAccountId: reporterAccount.id,
133 reason: body.reason,
134 state: AbuseState.PENDING,
135 predefinedReasons
136 }
137
138 if (body.video) {
139 return createVideoAbuse({
140 baseAbuse,
141 videoInstance,
142 reporterAccount,
143 transaction: t,
144 startAt: body.video.startAt,
145 endAt: body.video.endAt
146 })
147 }
148
149 if (body.comment) {
150 return createVideoCommentAbuse({
151 baseAbuse,
152 commentInstance,
153 reporterAccount,
154 transaction: t
155 })
156 }
157
158 // Account report
159 return createAccountAbuse({
160 baseAbuse,
161 accountInstance,
162 reporterAccount,
163 transaction: t
164 })
165 })
166
167 return res.json({ abuse: { id } })
168}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index c334a26b4..eda9e04d1 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -3,6 +3,7 @@ import * as express from 'express'
3import * as RateLimit from 'express-rate-limit' 3import * as RateLimit from 'express-rate-limit'
4import { badRequest } from '../../helpers/express-utils' 4import { badRequest } from '../../helpers/express-utils'
5import { CONFIG } from '../../initializers/config' 5import { CONFIG } from '../../initializers/config'
6import { abuseRouter } from './abuse'
6import { accountsRouter } from './accounts' 7import { accountsRouter } from './accounts'
7import { bulkRouter } from './bulk' 8import { bulkRouter } from './bulk'
8import { configRouter } from './config' 9import { configRouter } from './config'
@@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({
32apiRouter.use(apiRateLimiter) 33apiRouter.use(apiRateLimiter)
33 34
34apiRouter.use('/server', serverRouter) 35apiRouter.use('/server', serverRouter)
36apiRouter.use('/abuses', abuseRouter)
35apiRouter.use('/bulk', bulkRouter) 37apiRouter.use('/bulk', bulkRouter)
36apiRouter.use('/oauth-clients', oauthClientsRouter) 38apiRouter.use('/oauth-clients', oauthClientsRouter)
37apiRouter.use('/config', configRouter) 39apiRouter.use('/config', configRouter)
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
index 77a15e5fc..dc915977f 100644
--- a/server/controllers/api/users/my-history.ts
+++ b/server/controllers/api/users/my-history.ts
@@ -50,7 +50,5 @@ async function removeUserHistory (req: express.Request, res: express.Response) {
50 return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t) 50 return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t)
51 }) 51 })
52 52
53 // Do not send the delete to other instances, we delete OUR copy of this video abuse
54
55 return res.type('json').status(204).end() 53 return res.type('json').status(204).end()
56} 54}
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 017f5219e..0be51c128 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -68,7 +68,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
68 const values: UserNotificationSetting = { 68 const values: UserNotificationSetting = {
69 newVideoFromSubscription: body.newVideoFromSubscription, 69 newVideoFromSubscription: body.newVideoFromSubscription,
70 newCommentOnMyVideo: body.newCommentOnMyVideo, 70 newCommentOnMyVideo: body.newCommentOnMyVideo,
71 videoAbuseAsModerator: body.videoAbuseAsModerator, 71 abuseAsModerator: body.abuseAsModerator,
72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, 72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
73 blacklistOnMyVideo: body.blacklistOnMyVideo, 73 blacklistOnMyVideo: body.blacklistOnMyVideo,
74 myVideoPublished: body.myVideoPublished, 74 myVideoPublished: body.myVideoPublished,
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index ab2074459..b92a66360 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,9 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared' 2import { AbuseModel } from '@server/models/abuse/abuse'
3import { logger } from '../../../helpers/logger' 3import { getServerActor } from '@server/models/application/application'
4import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
4import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers/database'
6import { 6import {
7 abusesSortValidator,
7 asyncMiddleware, 8 asyncMiddleware,
8 asyncRetryTransactionMiddleware, 9 asyncRetryTransactionMiddleware,
9 authenticate, 10 authenticate,
@@ -12,28 +13,21 @@ import {
12 setDefaultPagination, 13 setDefaultPagination,
13 setDefaultSort, 14 setDefaultSort,
14 videoAbuseGetValidator, 15 videoAbuseGetValidator,
16 videoAbuseListValidator,
15 videoAbuseReportValidator, 17 videoAbuseReportValidator,
16 videoAbusesSortValidator, 18 videoAbuseUpdateValidator
17 videoAbuseUpdateValidator,
18 videoAbuseListValidator
19} from '../../../middlewares' 19} from '../../../middlewares'
20import { AccountModel } from '../../../models/account/account' 20import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse'
21import { VideoAbuseModel } from '../../../models/video/video-abuse' 21
22import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 22// FIXME: deprecated in 2.3. Remove this controller
23import { Notifier } from '../../../lib/notifier'
24import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
25import { MVideoAbuseAccountVideo } from '../../../types/models/video'
26import { getServerActor } from '@server/models/application/application'
27import { MAccountDefault } from '@server/types/models'
28 23
29const auditLogger = auditLoggerFactory('abuse')
30const abuseVideoRouter = express.Router() 24const abuseVideoRouter = express.Router()
31 25
32abuseVideoRouter.get('/abuse', 26abuseVideoRouter.get('/abuse',
33 authenticate, 27 authenticate,
34 ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), 28 ensureUserHasRight(UserRight.MANAGE_ABUSES),
35 paginationValidator, 29 paginationValidator,
36 videoAbusesSortValidator, 30 abusesSortValidator,
37 setDefaultSort, 31 setDefaultSort,
38 setDefaultPagination, 32 setDefaultPagination,
39 videoAbuseListValidator, 33 videoAbuseListValidator,
@@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse',
41) 35)
42abuseVideoRouter.put('/:videoId/abuse/:id', 36abuseVideoRouter.put('/:videoId/abuse/:id',
43 authenticate, 37 authenticate,
44 ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), 38 ensureUserHasRight(UserRight.MANAGE_ABUSES),
45 asyncMiddleware(videoAbuseUpdateValidator), 39 asyncMiddleware(videoAbuseUpdateValidator),
46 asyncRetryTransactionMiddleware(updateVideoAbuse) 40 asyncRetryTransactionMiddleware(updateVideoAbuse)
47) 41)
@@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse',
52) 46)
53abuseVideoRouter.delete('/:videoId/abuse/:id', 47abuseVideoRouter.delete('/:videoId/abuse/:id',
54 authenticate, 48 authenticate,
55 ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), 49 ensureUserHasRight(UserRight.MANAGE_ABUSES),
56 asyncMiddleware(videoAbuseGetValidator), 50 asyncMiddleware(videoAbuseGetValidator),
57 asyncRetryTransactionMiddleware(deleteVideoAbuse) 51 asyncRetryTransactionMiddleware(deleteVideoAbuse)
58) 52)
@@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
69 const user = res.locals.oauth.token.user 63 const user = res.locals.oauth.token.user
70 const serverActor = await getServerActor() 64 const serverActor = await getServerActor()
71 65
72 const resultList = await VideoAbuseModel.listForApi({ 66 const resultList = await AbuseModel.listForApi({
73 start: req.query.start, 67 start: req.query.start,
74 count: req.query.count, 68 count: req.query.count,
75 sort: req.query.sort, 69 sort: req.query.sort,
76 id: req.query.id, 70 id: req.query.id,
71 filter: 'video',
77 predefinedReason: req.query.predefinedReason, 72 predefinedReason: req.query.predefinedReason,
78 search: req.query.search, 73 search: req.query.search,
79 state: req.query.state, 74 state: req.query.state,
@@ -90,74 +85,28 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
90} 85}
91 86
92async function updateVideoAbuse (req: express.Request, res: express.Response) { 87async function updateVideoAbuse (req: express.Request, res: express.Response) {
93 const videoAbuse = res.locals.videoAbuse 88 return updateAbuse(req, res)
94
95 if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
96 if (req.body.state !== undefined) videoAbuse.state = req.body.state
97
98 await sequelizeTypescript.transaction(t => {
99 return videoAbuse.save({ transaction: t })
100 })
101
102 // Do not send the delete to other instances, we updated OUR copy of this video abuse
103
104 return res.type('json').status(204).end()
105} 89}
106 90
107async function deleteVideoAbuse (req: express.Request, res: express.Response) { 91async function deleteVideoAbuse (req: express.Request, res: express.Response) {
108 const videoAbuse = res.locals.videoAbuse 92 return deleteAbuse(req, res)
109
110 await sequelizeTypescript.transaction(t => {
111 return videoAbuse.destroy({ transaction: t })
112 })
113
114 // Do not send the delete to other instances, we delete OUR copy of this video abuse
115
116 return res.type('json').status(204).end()
117} 93}
118 94
119async function reportVideoAbuse (req: express.Request, res: express.Response) { 95async function reportVideoAbuse (req: express.Request, res: express.Response) {
120 const videoInstance = res.locals.videoAll 96 const oldBody = req.body as VideoAbuseCreate
121 const body: VideoAbuseCreate = req.body
122 let reporterAccount: MAccountDefault
123 let videoAbuseJSON: VideoAbuse
124
125 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
126 reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
127 const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
128
129 const abuseToCreate = {
130 reporterAccountId: reporterAccount.id,
131 reason: body.reason,
132 videoId: videoInstance.id,
133 state: VideoAbuseState.PENDING,
134 predefinedReasons,
135 startAt: body.startAt,
136 endAt: body.endAt
137 }
138
139 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
140 videoAbuseInstance.Video = videoInstance
141 videoAbuseInstance.Account = reporterAccount
142
143 // We send the video abuse to the origin server
144 if (videoInstance.isOwned() === false) {
145 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
146 }
147 97
148 videoAbuseJSON = videoAbuseInstance.toFormattedJSON() 98 req.body = {
149 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON)) 99 accountId: res.locals.videoAll.VideoChannel.accountId,
150 100
151 return videoAbuseInstance 101 reason: oldBody.reason,
152 }) 102 predefinedReasons: oldBody.predefinedReasons,
153 103
154 Notifier.Instance.notifyOnNewVideoAbuse({ 104 video: {
155 videoAbuse: videoAbuseJSON, 105 id: res.locals.videoAll.id,
156 videoAbuseInstance, 106 startAt: oldBody.startAt,
157 reporter: reporterAccount.Actor.getIdentifier() 107 endAt: oldBody.endAt
158 }) 108 }
159 109 } as AbuseCreate
160 logger.info('Abuse report for video "%s" created.', videoInstance.name)
161 110
162 return res.json({ videoAbuse: videoAbuseJSON }).end() 111 return reportAbuse(req, res)
163} 112}
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 0bbfbc753..954b0b69d 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -1,15 +1,15 @@
1import * as path from 'path'
2import * as express from 'express'
3import { diff } from 'deep-object-diff' 1import { diff } from 'deep-object-diff'
4import { chain } from 'lodash' 2import * as express from 'express'
5import * as flatten from 'flat' 3import * as flatten from 'flat'
4import { chain } from 'lodash'
5import * as path from 'path'
6import * as winston from 'winston' 6import * as winston from 'winston'
7import { jsonLoggerFormat, labelFormatter } from './logger' 7import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
8import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared' 8import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
9import { VideoComment } from '../../shared/models/videos/video-comment.model'
10import { CustomConfig } from '../../shared/models/server/custom-config.model' 9import { CustomConfig } from '../../shared/models/server/custom-config.model'
10import { VideoComment } from '../../shared/models/videos/video-comment.model'
11import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
12import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' 12import { jsonLoggerFormat, labelFormatter } from './logger'
13 13
14function getAuditIdFromRes (res: express.Response) { 14function getAuditIdFromRes (res: express.Response) {
15 return res.locals.oauth.token.User.username 15 return res.locals.oauth.token.User.username
@@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView {
212 } 212 }
213} 213}
214 214
215const videoAbuseKeysToKeep = [ 215const abuseKeysToKeep = [
216 'id', 216 'id',
217 'reason', 217 'reason',
218 'reporterAccount', 218 'reporterAccount',
219 'video-id',
220 'video-name',
221 'video-uuid',
222 'createdAt' 219 'createdAt'
223] 220]
224class VideoAbuseAuditView extends EntityAuditView { 221class AbuseAuditView extends EntityAuditView {
225 constructor (private readonly videoAbuse: VideoAbuse) { 222 constructor (private readonly abuse: Abuse) {
226 super(videoAbuseKeysToKeep, 'abuse', videoAbuse) 223 super(abuseKeysToKeep, 'abuse', abuse)
227 } 224 }
228} 225}
229 226
@@ -274,6 +271,6 @@ export {
274 CommentAuditView, 271 CommentAuditView,
275 UserAuditView, 272 UserAuditView,
276 VideoAuditView, 273 VideoAuditView,
277 VideoAbuseAuditView, 274 AbuseAuditView,
278 CustomConfigAuditView 275 CustomConfigAuditView
279} 276}
diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts
new file mode 100644
index 000000000..0ca06a252
--- /dev/null
+++ b/server/helpers/custom-validators/abuses.ts
@@ -0,0 +1,61 @@
1import validator from 'validator'
2import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs, AbuseCreate } from '@shared/models'
3import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
4import { exists, isArray } from './misc'
5
6const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
7
8function isAbuseReasonValid (value: string) {
9 return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON)
10}
11
12function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
13 return exists(value) && value in abusePredefinedReasonsMap
14}
15
16function isAbuseFilterValid (value: AbuseFilter) {
17 return value === 'video' || value === 'comment' || value === 'account'
18}
19
20function areAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
21 return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
22}
23
24function isAbuseTimestampValid (value: number) {
25 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
26}
27
28function isAbuseTimestampCoherent (endAt: number, { req }) {
29 const startAt = (req.body as AbuseCreate).video.startAt
30
31 return exists(startAt) && endAt > startAt
32}
33
34function isAbuseModerationCommentValid (value: string) {
35 return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
36}
37
38function isAbuseStateValid (value: string) {
39 return exists(value) && ABUSE_STATES[value] !== undefined
40}
41
42function isAbuseVideoIsValid (value: AbuseVideoIs) {
43 return exists(value) && (
44 value === 'deleted' ||
45 value === 'blacklisted'
46 )
47}
48
49// ---------------------------------------------------------------------------
50
51export {
52 isAbuseReasonValid,
53 isAbuseFilterValid,
54 isAbusePredefinedReasonValid,
55 areAbusePredefinedReasonsValid as isAbusePredefinedReasonsValid,
56 isAbuseTimestampValid,
57 isAbuseTimestampCoherent,
58 isAbuseModerationCommentValid,
59 isAbuseStateValid,
60 isAbuseVideoIsValid
61}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
index 6452e297c..dc90b3667 100644
--- a/server/helpers/custom-validators/activitypub/flag.ts
+++ b/server/helpers/custom-validators/activitypub/flag.ts
@@ -1,9 +1,9 @@
1import { isActivityPubUrlValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2import { isVideoAbuseReasonValid } from '../video-abuses' 2import { isAbuseReasonValid } from '../abuses'
3 3
4function isFlagActivityValid (activity: any) { 4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' && 5 return activity.type === 'Flag' &&
6 isVideoAbuseReasonValid(activity.content) && 6 isAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object) 7 isActivityPubUrlValid(activity.object)
8} 8}
9 9
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
deleted file mode 100644
index 0c2c34268..000000000
--- a/server/helpers/custom-validators/video-abuses.ts
+++ /dev/null
@@ -1,56 +0,0 @@
1import validator from 'validator'
2
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
4import { exists, isArray } from './misc'
5import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
6import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
7
8const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
9
10function isVideoAbuseReasonValid (value: string) {
11 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
12}
13
14function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) {
15 return exists(value) && value in videoAbusePredefinedReasonsMap
16}
17
18function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) {
19 return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap)
20}
21
22function isVideoAbuseTimestampValid (value: number) {
23 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
24}
25
26function isVideoAbuseTimestampCoherent (endAt: number, { req }) {
27 return exists(req.body.startAt) && endAt > req.body.startAt
28}
29
30function isVideoAbuseModerationCommentValid (value: string) {
31 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
32}
33
34function isVideoAbuseStateValid (value: string) {
35 return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined
36}
37
38function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
39 return exists(value) && (
40 value === 'deleted' ||
41 value === 'blacklisted'
42 )
43}
44
45// ---------------------------------------------------------------------------
46
47export {
48 isVideoAbuseReasonValid,
49 isVideoAbusePredefinedReasonValid,
50 isVideoAbusePredefinedReasonsValid,
51 isVideoAbuseTimestampValid,
52 isVideoAbuseTimestampCoherent,
53 isVideoAbuseModerationCommentValid,
54 isVideoAbuseStateValid,
55 isAbuseVideoIsValid
56}
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
index 846f28b17..455ff4241 100644
--- a/server/helpers/custom-validators/video-comments.ts
+++ b/server/helpers/custom-validators/video-comments.ts
@@ -1,6 +1,8 @@
1import 'multer' 1import * as express from 'express'
2import validator from 'validator' 2import validator from 'validator'
3import { VideoCommentModel } from '@server/models/video/video-comment'
3import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
5import { MVideoId } from '@server/types/models'
4 6
5const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS 7const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
6 8
@@ -8,8 +10,83 @@ function isValidVideoCommentText (value: string) {
8 return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) 10 return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
9} 11}
10 12
13async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
14 const id = parseInt(idArg + '', 10)
15 const videoComment = await VideoCommentModel.loadById(id)
16
17 if (!videoComment) {
18 res.status(404)
19 .json({ error: 'Video comment thread not found' })
20 .end()
21
22 return false
23 }
24
25 if (videoComment.videoId !== video.id) {
26 res.status(400)
27 .json({ error: 'Video comment is not associated to this video.' })
28 .end()
29
30 return false
31 }
32
33 if (videoComment.inReplyToCommentId !== null) {
34 res.status(400)
35 .json({ error: 'Video comment is not a thread.' })
36 .end()
37
38 return false
39 }
40
41 res.locals.videoCommentThread = videoComment
42 return true
43}
44
45async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
46 const id = parseInt(idArg + '', 10)
47 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
48
49 if (!videoComment) {
50 res.status(404)
51 .json({ error: 'Video comment thread not found' })
52 .end()
53
54 return false
55 }
56
57 if (videoComment.videoId !== video.id) {
58 res.status(400)
59 .json({ error: 'Video comment is not associated to this video.' })
60 .end()
61
62 return false
63 }
64
65 res.locals.videoCommentFull = videoComment
66 return true
67}
68
69async function doesCommentIdExist (idArg: number | string, res: express.Response) {
70 const id = parseInt(idArg + '', 10)
71 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
72
73 if (!videoComment) {
74 res.status(404)
75 .json({ error: 'Video comment thread not found' })
76
77 return false
78 }
79
80 res.locals.videoCommentFull = videoComment
81
82 return true
83}
84
11// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
12 86
13export { 87export {
14 isValidVideoCommentText 88 isValidVideoCommentText,
89 doesVideoCommentThreadExist,
90 doesVideoCommentExist,
91 doesCommentIdExist
15} 92}
diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts
new file mode 100644
index 000000000..be8c8b449
--- /dev/null
+++ b/server/helpers/middlewares/abuses.ts
@@ -0,0 +1,47 @@
1import { Response } from 'express'
2import { AbuseModel } from '../../models/abuse/abuse'
3import { fetchVideo } from '../video'
4
5// FIXME: deprecated in 2.3. Remove this function
6async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
7 const abuseId = parseInt(abuseIdArg + '', 10)
8 let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
9
10 if (!abuse) {
11 const userId = res.locals.oauth?.token.User.id
12 const video = await fetchVideo(videoUUID, 'all', userId)
13
14 if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id)
15 }
16
17 if (abuse === null) {
18 res.status(404)
19 .json({ error: 'Video abuse not found' })
20
21 return false
22 }
23
24 res.locals.abuse = abuse
25 return true
26}
27
28async function doesAbuseExist (abuseId: number | string, res: Response) {
29 const abuse = await AbuseModel.loadById(parseInt(abuseId + '', 10))
30
31 if (!abuse) {
32 res.status(404)
33 .json({ error: 'Abuse not found' })
34
35 return false
36 }
37
38 res.locals.abuse = abuse
39 return true
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 doesAbuseExist,
46 doesVideoAbuseExist
47}
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts
index bddea7eaa..29b4ed1a6 100644
--- a/server/helpers/middlewares/accounts.ts
+++ b/server/helpers/middlewares/accounts.ts
@@ -3,8 +3,8 @@ import { AccountModel } from '../../models/account/account'
3import * as Bluebird from 'bluebird' 3import * as Bluebird from 'bluebird'
4import { MAccountDefault } from '../../types/models' 4import { MAccountDefault } from '../../types/models'
5 5
6function doesAccountIdExist (id: number, res: Response, sendNotFound = true) { 6function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
7 const promise = AccountModel.load(id) 7 const promise = AccountModel.load(parseInt(id + '', 10))
8 8
9 return doesAccountExist(promise, res, sendNotFound) 9 return doesAccountExist(promise, res, sendNotFound)
10} 10}
diff --git a/server/helpers/middlewares/index.ts b/server/helpers/middlewares/index.ts
index f91aeaa12..f57f3ad31 100644
--- a/server/helpers/middlewares/index.ts
+++ b/server/helpers/middlewares/index.ts
@@ -1,5 +1,5 @@
1export * from './abuses'
1export * from './accounts' 2export * from './accounts'
2export * from './video-abuses'
3export * from './video-blacklists' 3export * from './video-blacklists'
4export * from './video-captions' 4export * from './video-captions'
5export * from './video-channels' 5export * from './video-channels'
diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/video-abuses.ts
deleted file mode 100644
index 97a5724b6..000000000
--- a/server/helpers/middlewares/video-abuses.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import { Response } from 'express'
2import { VideoAbuseModel } from '../../models/video/video-abuse'
3import { fetchVideo } from '../video'
4
5async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
6 const abuseId = parseInt(abuseIdArg + '', 10)
7 let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
8
9 if (!videoAbuse) {
10 const userId = res.locals.oauth?.token.User.id
11 const video = await fetchVideo(videoUUID, 'all', userId)
12
13 if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id)
14 }
15
16 if (videoAbuse === null) {
17 res.status(404)
18 .json({ error: 'Video abuse not found' })
19 .end()
20
21 return false
22 }
23
24 res.locals.videoAbuse = videoAbuse
25 return true
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 doesVideoAbuseExist
32}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e730e3c84..2e9d3956e 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,9 +1,17 @@
1import { join } from 'path' 1import { join } from 'path'
2import { randomBytes } from 'crypto' 2import { randomBytes } from 'crypto'
3import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 3import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 4import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' 5import {
6 AbuseState,
7 VideoImportState,
8 VideoPrivacy,
9 VideoTranscodingFPS,
10 JobType,
11 VideoRateType,
12 VideoResolution,
13 VideoState
14} from '../../shared/models'
7// Do not use barrels, remain constants as independent as possible 15// Do not use barrels, remain constants as independent as possible
8import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils' 16import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 17import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -15,7 +23,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
15 23
16// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
17 25
18const LAST_MIGRATION_VERSION = 515 26const LAST_MIGRATION_VERSION = 520
19 27
20// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
21 29
@@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = {
51 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], 59 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
52 ACCOUNTS: [ 'createdAt' ], 60 ACCOUNTS: [ 'createdAt' ],
53 JOBS: [ 'createdAt' ], 61 JOBS: [ 'createdAt' ],
54 VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
55 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 62 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
56 VIDEO_IMPORTS: [ 'createdAt' ], 63 VIDEO_IMPORTS: [ 'createdAt' ],
57 VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], 64 VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
@@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = {
66 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], 73 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
67 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], 74 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
68 75
76 ABUSES: [ 'id', 'createdAt', 'state' ],
77
69 ACCOUNTS_BLOCKLIST: [ 'createdAt' ], 78 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
70 SERVERS_BLOCKLIST: [ 'createdAt' ], 79 SERVERS_BLOCKLIST: [ 'createdAt' ],
71 80
@@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = {
193 VIDEO_LANGUAGES: { max: 500 }, // Array length 202 VIDEO_LANGUAGES: { max: 500 }, // Array length
194 BLOCKED_REASON: { min: 3, max: 250 } // Length 203 BLOCKED_REASON: { min: 3, max: 250 } // Length
195 }, 204 },
196 VIDEO_ABUSES: { 205 ABUSES: {
197 REASON: { min: 2, max: 3000 }, // Length 206 REASON: { min: 2, max: 3000 }, // Length
198 MODERATION_COMMENT: { min: 2, max: 3000 } // Length 207 MODERATION_COMMENT: { min: 2, max: 3000 } // Length
199 }, 208 },
@@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = {
378 [VideoImportState.REJECTED]: 'Rejected' 387 [VideoImportState.REJECTED]: 'Rejected'
379} 388}
380 389
381const VIDEO_ABUSE_STATES = { 390const ABUSE_STATES = {
382 [VideoAbuseState.PENDING]: 'Pending', 391 [AbuseState.PENDING]: 'Pending',
383 [VideoAbuseState.REJECTED]: 'Rejected', 392 [AbuseState.REJECTED]: 'Rejected',
384 [VideoAbuseState.ACCEPTED]: 'Accepted' 393 [AbuseState.ACCEPTED]: 'Accepted'
385} 394}
386 395
387const VIDEO_PLAYLIST_PRIVACIES = { 396const VIDEO_PLAYLIST_PRIVACIES = {
@@ -778,7 +787,7 @@ export {
778 VIDEO_RATE_TYPES, 787 VIDEO_RATE_TYPES,
779 VIDEO_TRANSCODING_FPS, 788 VIDEO_TRANSCODING_FPS,
780 FFMPEG_NICE, 789 FFMPEG_NICE,
781 VIDEO_ABUSE_STATES, 790 ABUSE_STATES,
782 VIDEO_CHANNELS, 791 VIDEO_CHANNELS,
783 LRU_CACHE, 792 LRU_CACHE,
784 JOB_REQUEST_TIMEOUT, 793 JOB_REQUEST_TIMEOUT,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 633d4f956..0775f1fad 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -1,44 +1,45 @@
1import { QueryTypes, Transaction } from 'sequelize'
1import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' 2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { AbuseModel } from '@server/models/abuse/abuse'
4import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
5import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
2import { isTestInstance } from '../helpers/core-utils' 6import { isTestInstance } from '../helpers/core-utils'
3import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
4
5import { AccountModel } from '../models/account/account' 8import { AccountModel } from '../models/account/account'
9import { AccountBlocklistModel } from '../models/account/account-blocklist'
6import { AccountVideoRateModel } from '../models/account/account-video-rate' 10import { AccountVideoRateModel } from '../models/account/account-video-rate'
7import { UserModel } from '../models/account/user' 11import { UserModel } from '../models/account/user'
12import { UserNotificationModel } from '../models/account/user-notification'
13import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
14import { UserVideoHistoryModel } from '../models/account/user-video-history'
8import { ActorModel } from '../models/activitypub/actor' 15import { ActorModel } from '../models/activitypub/actor'
9import { ActorFollowModel } from '../models/activitypub/actor-follow' 16import { ActorFollowModel } from '../models/activitypub/actor-follow'
10import { ApplicationModel } from '../models/application/application' 17import { ApplicationModel } from '../models/application/application'
11import { AvatarModel } from '../models/avatar/avatar' 18import { AvatarModel } from '../models/avatar/avatar'
12import { OAuthClientModel } from '../models/oauth/oauth-client' 19import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { OAuthTokenModel } from '../models/oauth/oauth-token' 20import { OAuthTokenModel } from '../models/oauth/oauth-token'
21import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
22import { PluginModel } from '../models/server/plugin'
14import { ServerModel } from '../models/server/server' 23import { ServerModel } from '../models/server/server'
24import { ServerBlocklistModel } from '../models/server/server-blocklist'
25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
15import { TagModel } from '../models/video/tag' 26import { TagModel } from '../models/video/tag'
27import { ThumbnailModel } from '../models/video/thumbnail'
16import { VideoModel } from '../models/video/video' 28import { VideoModel } from '../models/video/video'
17import { VideoAbuseModel } from '../models/video/video-abuse'
18import { VideoBlacklistModel } from '../models/video/video-blacklist' 29import { VideoBlacklistModel } from '../models/video/video-blacklist'
30import { VideoCaptionModel } from '../models/video/video-caption'
31import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
19import { VideoChannelModel } from '../models/video/video-channel' 32import { VideoChannelModel } from '../models/video/video-channel'
20import { VideoCommentModel } from '../models/video/video-comment' 33import { VideoCommentModel } from '../models/video/video-comment'
21import { VideoFileModel } from '../models/video/video-file' 34import { VideoFileModel } from '../models/video/video-file'
22import { VideoShareModel } from '../models/video/video-share'
23import { VideoTagModel } from '../models/video/video-tag'
24import { CONFIG } from './config'
25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
26import { VideoCaptionModel } from '../models/video/video-caption'
27import { VideoImportModel } from '../models/video/video-import' 35import { VideoImportModel } from '../models/video/video-import'
28import { VideoViewModel } from '../models/video/video-view'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history'
32import { AccountBlocklistModel } from '../models/account/account-blocklist'
33import { ServerBlocklistModel } from '../models/server/server-blocklist'
34import { UserNotificationModel } from '../models/account/user-notification'
35import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
36import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
37import { VideoPlaylistModel } from '../models/video/video-playlist' 36import { VideoPlaylistModel } from '../models/video/video-playlist'
38import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' 37import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
39import { ThumbnailModel } from '../models/video/thumbnail' 38import { VideoShareModel } from '../models/video/video-share'
40import { PluginModel } from '../models/server/plugin' 39import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
41import { QueryTypes, Transaction } from 'sequelize' 40import { VideoTagModel } from '../models/video/video-tag'
41import { VideoViewModel } from '../models/video/video-view'
42import { CONFIG } from './config'
42 43
43require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 44require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
44 45
@@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) {
86 TagModel, 87 TagModel,
87 AccountVideoRateModel, 88 AccountVideoRateModel,
88 UserModel, 89 UserModel,
90 AbuseModel,
91 VideoCommentAbuseModel,
89 VideoAbuseModel, 92 VideoAbuseModel,
90 VideoModel, 93 VideoModel,
91 VideoChangeOwnershipModel, 94 VideoChangeOwnershipModel,
diff --git a/server/initializers/migrations/0250-video-abuse-state.ts b/server/initializers/migrations/0250-video-abuse-state.ts
index 50de25182..e4993c393 100644
--- a/server/initializers/migrations/0250-video-abuse-state.ts
+++ b/server/initializers/migrations/0250-video-abuse-state.ts
@@ -1,5 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoAbuseState } from '../../../shared/models/videos' 2import { AbuseState } from '../../../shared/models'
3 3
4async function up (utils: { 4async function up (utils: {
5 transaction: Sequelize.Transaction 5 transaction: Sequelize.Transaction
@@ -16,7 +16,7 @@ async function up (utils: {
16 } 16 }
17 17
18 { 18 {
19 const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING 19 const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING
20 await utils.sequelize.query(query) 20 await utils.sequelize.query(query)
21 } 21 }
22 22
diff --git a/server/initializers/migrations/0470-cleaup-indexes.ts b/server/initializers/migrations/0470-cleanup-indexes.ts
index 7365c30f8..7365c30f8 100644
--- a/server/initializers/migrations/0470-cleaup-indexes.ts
+++ b/server/initializers/migrations/0470-cleanup-indexes.ts
diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts
new file mode 100644
index 000000000..b02a21989
--- /dev/null
+++ b/server/initializers/migrations/0520-abuses-split.ts
@@ -0,0 +1,90 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 await utils.queryInterface.renameTable('videoAbuse', 'abuse')
9
10 await utils.sequelize.query(`
11 ALTER TABLE "abuse"
12 ADD COLUMN "flaggedAccountId" INTEGER REFERENCES "account" ("id") ON DELETE SET NULL ON UPDATE CASCADE
13 `)
14
15 await utils.sequelize.query(`
16 UPDATE "abuse" SET "videoId" = NULL
17 WHERE "videoId" NOT IN (SELECT "id" FROM "video")
18 `)
19
20 await utils.sequelize.query(`
21 UPDATE "abuse" SET "flaggedAccountId" = "videoChannel"."accountId"
22 FROM "video" INNER JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"
23 WHERE "abuse"."videoId" = "video"."id"
24 `)
25
26 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_video_id;')
27 await utils.sequelize.query('DROP INDEX IF EXISTS video_abuse_reporter_account_id;')
28
29 await utils.sequelize.query(`
30 CREATE TABLE IF NOT EXISTS "videoAbuse" (
31 "id" serial,
32 "startAt" integer DEFAULT NULL,
33 "endAt" integer DEFAULT NULL,
34 "deletedVideo" jsonb DEFAULT NULL,
35 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
36 "videoId" integer REFERENCES "video" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
37 "createdAt" TIMESTAMP WITH time zone NOT NULL,
38 "updatedAt" timestamp WITH time zone NOT NULL,
39 PRIMARY KEY ("id")
40 );
41 `)
42
43 await utils.sequelize.query(`
44 CREATE TABLE IF NOT EXISTS "commentAbuse" (
45 "id" serial,
46 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
47 "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
48 "createdAt" timestamp WITH time zone NOT NULL,
49 "updatedAt" timestamp WITH time zone NOT NULL,
50 PRIMARY KEY ("id")
51 );
52 `)
53
54 await utils.sequelize.query(`
55 INSERT INTO "videoAbuse" ("startAt", "endAt", "deletedVideo", "abuseId", "videoId", "createdAt", "updatedAt")
56 SELECT "abuse"."startAt", "abuse"."endAt", "abuse"."deletedVideo", "abuse"."id", "abuse"."videoId",
57 "abuse"."createdAt", "abuse"."updatedAt"
58 FROM "abuse"
59 `)
60
61 await utils.queryInterface.removeColumn('abuse', 'startAt')
62 await utils.queryInterface.removeColumn('abuse', 'endAt')
63 await utils.queryInterface.removeColumn('abuse', 'deletedVideo')
64 await utils.queryInterface.removeColumn('abuse', 'videoId')
65
66 await utils.sequelize.query('DROP INDEX IF EXISTS user_notification_video_abuse_id')
67 await utils.queryInterface.renameColumn('userNotification', 'videoAbuseId', 'abuseId')
68 await utils.sequelize.query(
69 'ALTER TABLE "userNotification" RENAME CONSTRAINT "userNotification_videoAbuseId_fkey" TO "userNotification_abuseId_fkey"'
70 )
71
72 await utils.sequelize.query(
73 'ALTER TABLE "abuse" RENAME CONSTRAINT "videoAbuse_reporterAccountId_fkey" TO "abuse_reporterAccountId_fkey"'
74 )
75
76 await utils.sequelize.query(
77 'ALTER INDEX IF EXISTS "videoAbuse_pkey" RENAME TO "abuse_pkey"'
78 )
79
80 await utils.queryInterface.renameColumn('userNotificationSetting', 'videoAbuseAsModerator', 'abuseAsModerator')
81}
82
83function down (options) {
84 throw new Error('Not implemented.')
85}
86
87export {
88 up,
89 down
90}
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 1d7132a3a..6350cee12 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -1,24 +1,19 @@
1import { 1import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
2 ActivityCreate, 2import { AccountModel } from '@server/models/account/account'
3 ActivityFlag, 3import { VideoModel } from '@server/models/video/video'
4 VideoAbuseState, 4import { VideoCommentModel } from '@server/models/video/video-comment'
5 videoAbusePredefinedReasonsMap 5import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
6} from '../../../../shared' 6import { getAPId } from '../../../helpers/activitypub'
7import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
8import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { sequelizeTypescript } from '../../../initializers/database' 9import { sequelizeTypescript } from '../../../initializers/database'
11import { VideoAbuseModel } from '../../../models/video/video-abuse'
12import { getOrCreateVideoAndAccountAndChannel } from '../videos'
13import { Notifier } from '../../notifier'
14import { getAPId } from '../../../helpers/activitypub'
15import { APProcessorOptions } from '../../../types/activitypub-processor.model' 10import { APProcessorOptions } from '../../../types/activitypub-processor.model'
16import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models' 11import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
17import { AccountModel } from '@server/models/account/account'
18 12
19async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
20 const { activity, byActor } = options 14 const { activity, byActor } = options
21 return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) 15
16 return retryTransactionWrapper(processCreateAbuse, activity, byActor)
22} 17}
23 18
24// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
@@ -29,55 +24,79 @@ export {
29 24
30// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
31 26
32async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { 27async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
33 const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) 28 const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
34 29
35 const account = byActor.Account 30 const account = byActor.Account
36 if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url) 31 if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
32
33 const reporterAccount = await AccountModel.load(account.id)
37 34
38 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] 35 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
39 36
37 const tags = Array.isArray(flag.tag) ? flag.tag : []
38 const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name])
39 .filter(v => !isNaN(v))
40
41 const startAt = flag.startAt
42 const endAt = flag.endAt
43
40 for (const object of objects) { 44 for (const object of objects) {
41 try { 45 try {
42 logger.debug('Reporting remote abuse for video %s.', getAPId(object)) 46 const uri = getAPId(object)
43
44 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
45 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
46 const tags = Array.isArray(flag.tag) ? flag.tag : []
47 const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
48 .filter(v => !isNaN(v))
49 const startAt = flag.startAt
50 const endAt = flag.endAt
51
52 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
53 const videoAbuseData = {
54 reporterAccountId: account.id,
55 reason: flag.content,
56 videoId: video.id,
57 state: VideoAbuseState.PENDING,
58 predefinedReasons,
59 startAt,
60 endAt
61 }
62 47
63 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) 48 logger.debug('Reporting remote abuse for object %s.', uri)
64 videoAbuseInstance.Video = video
65 videoAbuseInstance.Account = reporterAccount
66 49
67 logger.info('Remote abuse for video uuid %s created', flag.object) 50 await sequelizeTypescript.transaction(async t => {
68 51
69 return videoAbuseInstance 52 const video = await VideoModel.loadByUrlAndPopulateAccount(uri)
70 }) 53 let videoComment: MCommentOwnerVideo
54 let flaggedAccount: MAccountDefault
55
56 if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri)
57 if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri)
58
59 if (!video && !videoComment && !flaggedAccount) {
60 logger.warn('Cannot flag unknown entity %s.', object)
61 return
62 }
63
64 const baseAbuse = {
65 reporterAccountId: reporterAccount.id,
66 reason: flag.content,
67 state: AbuseState.PENDING,
68 predefinedReasons
69 }
71 70
72 const videoAbuseJSON = videoAbuseInstance.toFormattedJSON() 71 if (video) {
72 return createVideoAbuse({
73 baseAbuse,
74 startAt,
75 endAt,
76 reporterAccount,
77 transaction: t,
78 videoInstance: video
79 })
80 }
81
82 if (videoComment) {
83 return createVideoCommentAbuse({
84 baseAbuse,
85 reporterAccount,
86 transaction: t,
87 commentInstance: videoComment
88 })
89 }
73 90
74 Notifier.Instance.notifyOnNewVideoAbuse({ 91 return await createAccountAbuse({
75 videoAbuse: videoAbuseJSON, 92 baseAbuse,
76 videoAbuseInstance, 93 reporterAccount,
77 reporter: reporterAccount.Actor.getIdentifier() 94 transaction: t,
95 accountInstance: flaggedAccount
96 })
78 }) 97 })
79 } catch (err) { 98 } catch (err) {
80 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) 99 logger.debug('Cannot process report of %s', getAPId(object), { err })
81 } 100 }
82 } 101 }
83} 102}
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index 3a1fe0812..821637ec8 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -1,32 +1,31 @@
1import { getVideoAbuseActivityPubUrl } from '../url' 1import { Transaction } from 'sequelize'
2import { unicastTo } from './utils'
3import { logger } from '../../../helpers/logger'
4import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
3import { logger } from '../../../helpers/logger'
4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
5import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
6import { Transaction } from 'sequelize' 6import { getAbuseActivityPubUrl } from '../url'
7import { MActor, MVideoFullLight } from '../../../types/models' 7import { unicastTo } from './utils'
8import { MVideoAbuseVideo } from '../../../types/models/video'
9 8
10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 9function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 10 if (!flaggedAccount.Actor.serverId) return // Local user
12 11
13 const url = getVideoAbuseActivityPubUrl(videoAbuse) 12 const url = getAbuseActivityPubUrl(abuse)
14 13
15 logger.info('Creating job to send video abuse %s.', url) 14 logger.info('Creating job to send abuse %s.', url)
16 15
17 // Custom audience, we only send the abuse to the origin instance 16 // Custom audience, we only send the abuse to the origin instance
18 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } 17 const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
19 const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) 18 const flagActivity = buildFlagActivity(url, byActor, abuse, audience)
20 19
21 t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox())) 20 t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox()))
22} 21}
23 22
24function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag { 23function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag {
25 if (!audience) audience = getAudience(byActor) 24 if (!audience) audience = getAudience(byActor)
26 25
27 const activity = Object.assign( 26 const activity = Object.assign(
28 { id: url, actor: byActor.url }, 27 { id: url, actor: byActor.url },
29 videoAbuse.toActivityPubObject() 28 abuse.toActivityPubObject()
30 ) 29 )
31 30
32 return audiencify(activity, audience) 31 return audiencify(activity, audience)
@@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus
35// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
36 35
37export { 36export {
38 sendVideoAbuse 37 sendAbuse
39} 38}
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index 7f98751a1..b54e038a4 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -5,10 +5,10 @@ import {
5 MActorId, 5 MActorId,
6 MActorUrl, 6 MActorUrl,
7 MCommentId, 7 MCommentId,
8 MVideoAbuseId,
9 MVideoId, 8 MVideoId,
10 MVideoUrl, 9 MVideoUrl,
11 MVideoUUID 10 MVideoUUID,
11 MAbuseId
12} from '../../types/models' 12} from '../../types/models'
13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' 13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
14import { MVideoFileVideoUUID } from '../../types/models/video/video-file' 14import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
@@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) {
48 return WEBSERVER.URL + '/accounts/' + accountName 48 return WEBSERVER.URL + '/accounts/' + accountName
49} 49}
50 50
51function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) { 51function getAbuseActivityPubUrl (abuse: MAbuseId) {
52 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id 52 return WEBSERVER.URL + '/admin/abuses/' + abuse.id
53} 53}
54 54
55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { 55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
@@ -118,7 +118,7 @@ export {
118 getVideoCacheStreamingPlaylistActivityPubUrl, 118 getVideoCacheStreamingPlaylistActivityPubUrl,
119 getVideoChannelActivityPubUrl, 119 getVideoChannelActivityPubUrl,
120 getAccountActivityPubUrl, 120 getAccountActivityPubUrl,
121 getVideoAbuseActivityPubUrl, 121 getAbuseActivityPubUrl,
122 getActorFollowActivityPubUrl, 122 getActorFollowActivityPubUrl,
123 getActorFollowAcceptActivityPubUrl, 123 getActorFollowAcceptActivityPubUrl,
124 getVideoAnnounceActivityPubUrl, 124 getVideoAnnounceActivityPubUrl,
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index c08732b48..d54eab966 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,26 +1,20 @@
1import { readFileSync } from 'fs-extra'
2import { merge } from 'lodash'
1import { createTransport, Transporter } from 'nodemailer' 3import { createTransport, Transporter } from 'nodemailer'
4import { join } from 'path'
5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { Abuse, EmailPayload } from '@shared/models'
9import { SendEmailOptions } from '../../shared/models/server/emailer.model'
2import { isTestInstance, root } from '../helpers/core-utils' 10import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue'
6import { readFileSync } from 'fs-extra'
7import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
8import { 14import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
9 MCommentOwnerVideo, 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
10 MVideo, 16import { JobQueue } from './job-queue'
11 MVideoAbuseVideo, 17
12 MVideoAccountLight,
13 MVideoBlacklistLightVideo,
14 MVideoBlacklistVideo
15} from '../types/models/video'
16import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
17import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
18import { EmailPayload } from '@shared/models'
19import { join } from 'path'
20import { VideoAbuse } from '../../shared/models/videos'
21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
22import { merge } from 'lodash'
23import { VideoChannelModel } from '@server/models/video/video-channel'
24const Email = require('email-templates') 18const Email = require('email-templates')
25 19
26class Emailer { 20class Emailer {
@@ -288,28 +282,74 @@ class Emailer {
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 282 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 } 283 }
290 284
291 addVideoAbuseModeratorsNotification (to: string[], parameters: { 285 addAbuseModeratorsNotification (to: string[], parameters: {
292 videoAbuse: VideoAbuse 286 abuse: Abuse
293 videoAbuseInstance: MVideoAbuseVideo 287 abuseInstance: MAbuseFull
294 reporter: string 288 reporter: string
295 }) { 289 }) {
296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id 290 const { abuse, abuseInstance, reporter } = parameters
297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
298 291
299 const emailPayload: EmailPayload = { 292 const action = {
300 template: 'video-abuse-new', 293 text: 'View report #' + abuse.id,
301 to, 294 url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
302 subject: `New video abuse report from ${parameters.reporter}`, 295 }
303 locals: { 296
304 videoUrl, 297 let emailPayload: EmailPayload
305 videoAbuseUrl, 298
306 videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(), 299 if (abuseInstance.VideoAbuse) {
307 videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(), 300 const video = abuseInstance.VideoAbuse.Video
308 videoAbuse: parameters.videoAbuse, 301 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
309 reporter: parameters.reporter, 302
310 action: { 303 emailPayload = {
311 text: 'View report #' + parameters.videoAbuse.id, 304 template: 'video-abuse-new',
312 url: videoAbuseUrl 305 to,
306 subject: `New video abuse report from ${reporter}`,
307 locals: {
308 videoUrl,
309 isLocal: video.remote === false,
310 videoCreatedAt: new Date(video.createdAt).toLocaleString(),
311 videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
312 videoName: video.name,
313 reason: abuse.reason,
314 videoChannel: abuse.video.channel,
315 reporter,
316 action
317 }
318 }
319 } else if (abuseInstance.VideoCommentAbuse) {
320 const comment = abuseInstance.VideoCommentAbuse.VideoComment
321 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
322
323 emailPayload = {
324 template: 'video-comment-abuse-new',
325 to,
326 subject: `New comment abuse report from ${reporter}`,
327 locals: {
328 commentUrl,
329 videoName: comment.Video.name,
330 isLocal: comment.isOwned(),
331 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
332 reason: abuse.reason,
333 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
334 reporter,
335 action
336 }
337 }
338 } else {
339 const account = abuseInstance.FlaggedAccount
340 const accountUrl = account.getClientUrl()
341
342 emailPayload = {
343 template: 'account-abuse-new',
344 to,
345 subject: `New account abuse report from ${reporter}`,
346 locals: {
347 accountUrl,
348 accountDisplayName: account.getDisplayName(),
349 isLocal: account.isOwned(),
350 reason: abuse.reason,
351 reporter,
352 action
313 } 353 }
314 } 354 }
315 } 355 }
diff --git a/server/lib/emails/account-abuse-new/html.pug b/server/lib/emails/account-abuse-new/html.pug
new file mode 100644
index 000000000..f1aa2886e
--- /dev/null
+++ b/server/lib/emails/account-abuse-new/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | An account is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account
10 a(href=accountUrl) #{accountDisplayName}
11
12 p The reporter, #{reporter}, cited the following reason(s):
13 blockquote #{reason}
14 br(style="display: none;")
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug
index 76b805a24..831211864 100644
--- a/server/lib/emails/common/mixins.pug
+++ b/server/lib/emails/common/mixins.pug
@@ -1,3 +1,7 @@
1mixin channel(channel) 1mixin channel(channel)
2 - var handle = `${channel.name}@${channel.host}` 2 - var handle = `${channel.name}@${channel.host}`
3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file 3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
4
5mixin account(account)
6 - var handle = `${account.name}@${account.host}`
7 | #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug
index 999c89d26..a1acdabdc 100644
--- a/server/lib/emails/video-abuse-new/html.pug
+++ b/server/lib/emails/video-abuse-new/html.pug
@@ -6,13 +6,13 @@ block title
6 6
7block content 7block content
8 p 8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video " 9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
10 a(href=videoUrl) #{videoAbuse.video.name} 10 a(href=videoUrl) #{videoName}
11 | " by #[+channel(videoAbuse.video.channel)] 11 | " by #[+channel(videoChannel)]
12 if videoPublishedAt 12 if videoPublishedAt
13 | , published the #{videoPublishedAt}. 13 | , published the #{videoPublishedAt}.
14 else 14 else
15 | , uploaded the #{videoCreatedAt} but not yet published. 15 | , uploaded the #{videoCreatedAt} but not yet published.
16 p The reporter, #{reporter}, cited the following reason(s): 16 p The reporter, #{reporter}, cited the following reason(s):
17 blockquote #{videoAbuse.reason} 17 blockquote #{reason}
18 br(style="display: none;") 18 br(style="display: none;")
diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug
new file mode 100644
index 000000000..e92d986b5
--- /dev/null
+++ b/server/lib/emails/video-comment-abuse-new/html.pug
@@ -0,0 +1,16 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A comment is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}
10 a(href=commentUrl) comment on video "#{videoName}"
11 | of #{flaggedAccount}
12 | created on #{commentCreatedAt}
13
14 p The reporter, #{reporter}, cited the following reason(s):
15 blockquote #{reason}
16 br(style="display: none;")
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index 60d1b4053..4fc9cd747 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -1,15 +1,33 @@
1import { VideoModel } from '../models/video/video' 1import { PathLike } from 'fs-extra'
2import { VideoCommentModel } from '../models/video/video-comment' 2import { Transaction } from 'sequelize/types'
3import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' 3import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
4import { logger } from '@server/helpers/logger'
5import { AbuseModel } from '@server/models/abuse/abuse'
6import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
7import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { FilteredModelAttributes } from '@server/types'
10import {
11 MAbuseFull,
12 MAccountDefault,
13 MAccountLight,
14 MCommentAbuseAccountVideo,
15 MCommentOwnerVideo,
16 MUser,
17 MVideoAbuseVideoFull,
18 MVideoAccountLightBlacklistAllFiles
19} from '@server/types/models'
20import { ActivityCreate } from '../../shared/models/activitypub'
21import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
22import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
4import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' 23import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
24import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
5import { UserModel } from '../models/account/user' 25import { UserModel } from '../models/account/user'
6import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
7import { ActivityCreate } from '../../shared/models/activitypub'
8import { ActorModel } from '../models/activitypub/actor' 26import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' 27import { VideoModel } from '../models/video/video'
10import { VideoFileModel } from '@server/models/video/video-file' 28import { VideoCommentModel } from '../models/video/video-comment'
11import { PathLike } from 'fs-extra' 29import { sendAbuse } from './activitypub/send/send-flag'
12import { MUser } from '@server/types/models' 30import { Notifier } from './notifier'
13 31
14export type AcceptResult = { 32export type AcceptResult = {
15 accepted: boolean 33 accepted: boolean
@@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: {
73 return { accepted: true } 91 return { accepted: true }
74} 92}
75 93
94async function createVideoAbuse (options: {
95 baseAbuse: FilteredModelAttributes<AbuseModel>
96 videoInstance: MVideoAccountLightBlacklistAllFiles
97 startAt: number
98 endAt: number
99 transaction: Transaction
100 reporterAccount: MAccountDefault
101}) {
102 const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options
103
104 const associateFun = async (abuseInstance: MAbuseFull) => {
105 const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
106 abuseId: abuseInstance.id,
107 videoId: videoInstance.id,
108 startAt: startAt,
109 endAt: endAt
110 }, { transaction })
111
112 videoAbuseInstance.Video = videoInstance
113 abuseInstance.VideoAbuse = videoAbuseInstance
114
115 return { isOwned: videoInstance.isOwned() }
116 }
117
118 return createAbuse({
119 base: baseAbuse,
120 reporterAccount,
121 flaggedAccount: videoInstance.VideoChannel.Account,
122 transaction,
123 associateFun
124 })
125}
126
127function createVideoCommentAbuse (options: {
128 baseAbuse: FilteredModelAttributes<AbuseModel>
129 commentInstance: MCommentOwnerVideo
130 transaction: Transaction
131 reporterAccount: MAccountDefault
132}) {
133 const { baseAbuse, commentInstance, transaction, reporterAccount } = options
134
135 const associateFun = async (abuseInstance: MAbuseFull) => {
136 const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
137 abuseId: abuseInstance.id,
138 videoCommentId: commentInstance.id
139 }, { transaction })
140
141 commentAbuseInstance.VideoComment = commentInstance
142 abuseInstance.VideoCommentAbuse = commentAbuseInstance
143
144 return { isOwned: commentInstance.isOwned() }
145 }
146
147 return createAbuse({
148 base: baseAbuse,
149 reporterAccount,
150 flaggedAccount: commentInstance.Account,
151 transaction,
152 associateFun
153 })
154}
155
156function createAccountAbuse (options: {
157 baseAbuse: FilteredModelAttributes<AbuseModel>
158 accountInstance: MAccountDefault
159 transaction: Transaction
160 reporterAccount: MAccountDefault
161}) {
162 const { baseAbuse, accountInstance, transaction, reporterAccount } = options
163
164 const associateFun = async () => {
165 return { isOwned: accountInstance.isOwned() }
166 }
167
168 return createAbuse({
169 base: baseAbuse,
170 reporterAccount,
171 flaggedAccount: accountInstance,
172 transaction,
173 associateFun
174 })
175}
176
76export { 177export {
77 isLocalVideoAccepted, 178 isLocalVideoAccepted,
78 isLocalVideoThreadAccepted, 179 isLocalVideoThreadAccepted,
@@ -80,5 +181,48 @@ export {
80 isRemoteVideoCommentAccepted, 181 isRemoteVideoCommentAccepted,
81 isLocalVideoCommentReplyAccepted, 182 isLocalVideoCommentReplyAccepted,
82 isPreImportVideoAccepted, 183 isPreImportVideoAccepted,
83 isPostImportVideoAccepted 184 isPostImportVideoAccepted,
185
186 createAbuse,
187 createVideoAbuse,
188 createVideoCommentAbuse,
189 createAccountAbuse
190}
191
192// ---------------------------------------------------------------------------
193
194async function createAbuse (options: {
195 base: FilteredModelAttributes<AbuseModel>
196 reporterAccount: MAccountDefault
197 flaggedAccount: MAccountLight
198 associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} >
199 transaction: Transaction
200}) {
201 const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options
202 const auditLogger = auditLoggerFactory('abuse')
203
204 const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
205 const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction })
206
207 abuseInstance.ReporterAccount = reporterAccount
208 abuseInstance.FlaggedAccount = flaggedAccount
209
210 const { isOwned } = await associateFun(abuseInstance)
211
212 if (isOwned === false) {
213 await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
214 }
215
216 const abuseJSON = abuseInstance.toFormattedJSON()
217 auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
218
219 Notifier.Instance.notifyOnNewAbuse({
220 abuse: abuseJSON,
221 abuseInstance,
222 reporter: reporterAccount.Actor.getIdentifier()
223 })
224
225 logger.info('Abuse report %d created.', abuseInstance.id)
226
227 return abuseJSON
84} 228}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 943a087d2..c567e1c20 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -8,23 +8,18 @@ import {
8 MUserWithNotificationSetting, 8 MUserWithNotificationSetting,
9 UserNotificationModelForApi 9 UserNotificationModelForApi
10} from '@server/types/models/user' 10} from '@server/types/models/user'
11import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
11import { MVideoImportVideo } from '@server/types/models/video/video-import' 12import { MVideoImportVideo } from '@server/types/models/video/video-import'
13import { Abuse } from '@shared/models'
12import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' 14import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
13import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' 15import { VideoPrivacy, VideoState } from '../../shared/models/videos'
14import { logger } from '../helpers/logger' 16import { logger } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 17import { CONFIG } from '../initializers/config'
16import { AccountBlocklistModel } from '../models/account/account-blocklist' 18import { AccountBlocklistModel } from '../models/account/account-blocklist'
17import { UserModel } from '../models/account/user' 19import { UserModel } from '../models/account/user'
18import { UserNotificationModel } from '../models/account/user-notification' 20import { UserNotificationModel } from '../models/account/user-notification'
19import { MAccountServer, MActorFollowFull } from '../types/models' 21import { MAbuseFull, MAccountServer, MActorFollowFull } from '../types/models'
20import { 22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
21 MCommentOwnerVideo,
22 MVideoAbuseVideo,
23 MVideoAccountLight,
24 MVideoBlacklistLightVideo,
25 MVideoBlacklistVideo,
26 MVideoFullLight
27} from '../types/models/video'
28import { isBlockedByServerOrAccount } from './blocklist' 23import { isBlockedByServerOrAccount } from './blocklist'
29import { Emailer } from './emailer' 24import { Emailer } from './emailer'
30import { PeerTubeSocket } from './peertube-socket' 25import { PeerTubeSocket } from './peertube-socket'
@@ -78,9 +73,9 @@ class Notifier {
78 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 73 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
79 } 74 }
80 75
81 notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void { 76 notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
82 this.notifyModeratorsOfNewVideoAbuse(parameters) 77 this.notifyModeratorsOfNewAbuse(parameters)
83 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err })) 78 .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
84 } 79 }
85 80
86 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { 81 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
@@ -354,33 +349,39 @@ class Notifier {
354 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 349 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
355 } 350 }
356 351
357 private async notifyModeratorsOfNewVideoAbuse (parameters: { 352 private async notifyModeratorsOfNewAbuse (parameters: {
358 videoAbuse: VideoAbuse 353 abuse: Abuse
359 videoAbuseInstance: MVideoAbuseVideo 354 abuseInstance: MAbuseFull
360 reporter: string 355 reporter: string
361 }) { 356 }) {
362 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 357 const { abuse, abuseInstance } = parameters
358
359 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
363 if (moderators.length === 0) return 360 if (moderators.length === 0) return
364 361
365 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url) 362 const url = abuseInstance.VideoAbuse?.Video?.url ||
363 abuseInstance.VideoCommentAbuse?.VideoComment?.url ||
364 abuseInstance.FlaggedAccount.Actor.url
365
366 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
366 367
367 function settingGetter (user: MUserWithNotificationSetting) { 368 function settingGetter (user: MUserWithNotificationSetting) {
368 return user.NotificationSetting.videoAbuseAsModerator 369 return user.NotificationSetting.abuseAsModerator
369 } 370 }
370 371
371 async function notificationCreator (user: MUserWithNotificationSetting) { 372 async function notificationCreator (user: MUserWithNotificationSetting) {
372 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 373 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
373 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 374 type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS,
374 userId: user.id, 375 userId: user.id,
375 videoAbuseId: parameters.videoAbuse.id 376 abuseId: abuse.id
376 }) 377 })
377 notification.VideoAbuse = parameters.videoAbuseInstance 378 notification.Abuse = abuseInstance
378 379
379 return notification 380 return notification
380 } 381 }
381 382
382 function emailSender (emails: string[]) { 383 function emailSender (emails: string[]) {
383 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) 384 return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
384 } 385 }
385 386
386 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 387 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 43eef8ab1..642549879 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -133,7 +133,7 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
133 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 133 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
134 myVideoImportFinished: UserNotificationSettingValue.WEB, 134 myVideoImportFinished: UserNotificationSettingValue.WEB,
135 myVideoPublished: UserNotificationSettingValue.WEB, 135 myVideoPublished: UserNotificationSettingValue.WEB,
136 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 136 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
137 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 137 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
138 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 138 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
139 newUserRegistration: UserNotificationSettingValue.WEB, 139 newUserRegistration: UserNotificationSettingValue.WEB,
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
new file mode 100644
index 000000000..966d1f7fb
--- /dev/null
+++ b/server/middlewares/validators/abuse.ts
@@ -0,0 +1,277 @@
1import * as express from 'express'
2import { body, param, query } from 'express-validator'
3import {
4 isAbuseFilterValid,
5 isAbuseModerationCommentValid,
6 isAbusePredefinedReasonsValid,
7 isAbusePredefinedReasonValid,
8 isAbuseReasonValid,
9 isAbuseStateValid,
10 isAbuseTimestampCoherent,
11 isAbuseTimestampValid,
12 isAbuseVideoIsValid
13} from '@server/helpers/custom-validators/abuses'
14import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
15import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments'
16import { logger } from '@server/helpers/logger'
17import { doesAbuseExist, doesAccountIdExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
18import { AbuseCreate } from '@shared/models'
19import { areValidationErrors } from './utils'
20
21const abuseReportValidator = [
22 body('account.id')
23 .optional()
24 .custom(isIdValid)
25 .withMessage('Should have a valid accountId'),
26
27 body('video.id')
28 .optional()
29 .custom(isIdOrUUIDValid)
30 .withMessage('Should have a valid videoId'),
31 body('video.startAt')
32 .optional()
33 .customSanitizer(toIntOrNull)
34 .custom(isAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'),
36 body('video.endAt')
37 .optional()
38 .customSanitizer(toIntOrNull)
39 .custom(isAbuseTimestampValid)
40 .withMessage('Should have valid ending time value')
41 .bail()
42 .custom(isAbuseTimestampCoherent)
43 .withMessage('Should have a startAt timestamp beginning before endAt'),
44
45 body('comment.id')
46 .optional()
47 .custom(isIdValid)
48 .withMessage('Should have a valid commentId'),
49
50 body('reason')
51 .custom(isAbuseReasonValid)
52 .withMessage('Should have a valid reason'),
53
54 body('predefinedReasons')
55 .optional()
56 .custom(isAbusePredefinedReasonsValid)
57 .withMessage('Should have a valid list of predefined reasons'),
58
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking abuseReport parameters', { parameters: req.body })
61
62 if (areValidationErrors(req, res)) return
63
64 const body: AbuseCreate = req.body
65
66 if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
67 if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return
68 if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
69
70 if (!body.video?.id && !body.account?.id && !body.comment?.id) {
71 res.status(400)
72 .json({ error: 'video id or account id or comment id is required.' })
73
74 return
75 }
76
77 return next()
78 }
79]
80
81const abuseGetValidator = [
82 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
83
84 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
85 logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
86
87 if (areValidationErrors(req, res)) return
88 if (!await doesAbuseExist(req.params.id, res)) return
89
90 return next()
91 }
92]
93
94const abuseUpdateValidator = [
95 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
96
97 body('state')
98 .optional()
99 .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
100 body('moderationComment')
101 .optional()
102 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid moderation comment'),
103
104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
106
107 if (areValidationErrors(req, res)) return
108 if (!await doesAbuseExist(req.params.id, res)) return
109
110 return next()
111 }
112]
113
114const abuseListValidator = [
115 query('id')
116 .optional()
117 .custom(isIdValid).withMessage('Should have a valid id'),
118 query('filter')
119 .optional()
120 .custom(isAbuseFilterValid)
121 .withMessage('Should have a valid filter'),
122 query('predefinedReason')
123 .optional()
124 .custom(isAbusePredefinedReasonValid)
125 .withMessage('Should have a valid predefinedReason'),
126 query('search')
127 .optional()
128 .custom(exists).withMessage('Should have a valid search'),
129 query('state')
130 .optional()
131 .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
132 query('videoIs')
133 .optional()
134 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
135 query('searchReporter')
136 .optional()
137 .custom(exists).withMessage('Should have a valid reporter search'),
138 query('searchReportee')
139 .optional()
140 .custom(exists).withMessage('Should have a valid reportee search'),
141 query('searchVideo')
142 .optional()
143 .custom(exists).withMessage('Should have a valid video search'),
144 query('searchVideoChannel')
145 .optional()
146 .custom(exists).withMessage('Should have a valid video channel search'),
147
148 (req: express.Request, res: express.Response, next: express.NextFunction) => {
149 logger.debug('Checking abuseListValidator parameters', { parameters: req.body })
150
151 if (areValidationErrors(req, res)) return
152
153 return next()
154 }
155]
156
157// FIXME: deprecated in 2.3. Remove these validators
158
159const videoAbuseReportValidator = [
160 param('videoId')
161 .custom(isIdOrUUIDValid)
162 .not()
163 .isEmpty()
164 .withMessage('Should have a valid videoId'),
165 body('reason')
166 .custom(isAbuseReasonValid)
167 .withMessage('Should have a valid reason'),
168 body('predefinedReasons')
169 .optional()
170 .custom(isAbusePredefinedReasonsValid)
171 .withMessage('Should have a valid list of predefined reasons'),
172 body('startAt')
173 .optional()
174 .customSanitizer(toIntOrNull)
175 .custom(isAbuseTimestampValid)
176 .withMessage('Should have valid starting time value'),
177 body('endAt')
178 .optional()
179 .customSanitizer(toIntOrNull)
180 .custom(isAbuseTimestampValid)
181 .withMessage('Should have valid ending time value'),
182
183 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
184 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
185
186 if (areValidationErrors(req, res)) return
187 if (!await doesVideoExist(req.params.videoId, res)) return
188
189 return next()
190 }
191]
192
193const videoAbuseGetValidator = [
194 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
195 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
196
197 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
198 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
199
200 if (areValidationErrors(req, res)) return
201 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
202
203 return next()
204 }
205]
206
207const videoAbuseUpdateValidator = [
208 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
209 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
210 body('state')
211 .optional()
212 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
213 body('moderationComment')
214 .optional()
215 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
216
217 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
218 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
219
220 if (areValidationErrors(req, res)) return
221 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
222
223 return next()
224 }
225]
226
227const videoAbuseListValidator = [
228 query('id')
229 .optional()
230 .custom(isIdValid).withMessage('Should have a valid id'),
231 query('predefinedReason')
232 .optional()
233 .custom(isAbusePredefinedReasonValid)
234 .withMessage('Should have a valid predefinedReason'),
235 query('search')
236 .optional()
237 .custom(exists).withMessage('Should have a valid search'),
238 query('state')
239 .optional()
240 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
241 query('videoIs')
242 .optional()
243 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
244 query('searchReporter')
245 .optional()
246 .custom(exists).withMessage('Should have a valid reporter search'),
247 query('searchReportee')
248 .optional()
249 .custom(exists).withMessage('Should have a valid reportee search'),
250 query('searchVideo')
251 .optional()
252 .custom(exists).withMessage('Should have a valid video search'),
253 query('searchVideoChannel')
254 .optional()
255 .custom(exists).withMessage('Should have a valid video channel search'),
256
257 (req: express.Request, res: express.Response, next: express.NextFunction) => {
258 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
259
260 if (areValidationErrors(req, res)) return
261
262 return next()
263 }
264]
265
266// ---------------------------------------------------------------------------
267
268export {
269 abuseListValidator,
270 abuseReportValidator,
271 abuseGetValidator,
272 abuseUpdateValidator,
273 videoAbuseReportValidator,
274 videoAbuseGetValidator,
275 videoAbuseUpdateValidator,
276 videoAbuseListValidator
277}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 65dd00335..4086d77aa 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,3 +1,4 @@
1export * from './abuse'
1export * from './account' 2export * from './account'
2export * from './blocklist' 3export * from './blocklist'
3export * from './oembed' 4export * from './oembed'
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index b76dab722..29aba0436 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -5,7 +5,7 @@ import { checkSort, createSortableColumns } from './utils'
5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) 5const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
6const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) 6const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
7const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) 7const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
8const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 8const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES)
9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 9const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) 10const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) 11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
28const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 28const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
29const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 29const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
30const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 30const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
31const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 31const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
32const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 32const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
33const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 33const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
34const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 34const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
@@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL
52 52
53export { 53export {
54 usersSortValidator, 54 usersSortValidator,
55 videoAbusesSortValidator, 55 abusesSortValidator,
56 videoChannelsSortValidator, 56 videoChannelsSortValidator,
57 videoImportsSortValidator, 57 videoImportsSortValidator,
58 videosSearchSortValidator, 58 videosSearchSortValidator,
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
index fbfcb0a4c..21a7be08d 100644
--- a/server/middlewares/validators/user-notifications.ts
+++ b/server/middlewares/validators/user-notifications.ts
@@ -25,8 +25,8 @@ const updateNotificationSettingsValidator = [
25 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), 25 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'),
26 body('newCommentOnMyVideo') 26 body('newCommentOnMyVideo')
27 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), 27 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
28 body('videoAbuseAsModerator') 28 body('abuseAsModerator')
29 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), 29 .custom(isUserNotificationSettingValid).withMessage('Should have a valid abuse as moderator notification setting'),
30 body('videoAutoBlacklistAsModerator') 30 body('videoAutoBlacklistAsModerator')
31 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'), 31 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'),
32 body('blacklistOnMyVideo') 32 body('blacklistOnMyVideo')
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index a0d585b93..1eabada0a 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -1,4 +1,3 @@
1export * from './video-abuses'
2export * from './video-blacklist' 1export * from './video-blacklist'
3export * from './video-captions' 2export * from './video-captions'
4export * from './video-channels' 3export * from './video-channels'
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
deleted file mode 100644
index 5bbd1e3c6..000000000
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ /dev/null
@@ -1,135 +0,0 @@
1import * as express from 'express'
2import { body, param, query } from 'express-validator'
3import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
4import {
5 isAbuseVideoIsValid,
6 isVideoAbuseModerationCommentValid,
7 isVideoAbuseReasonValid,
8 isVideoAbuseStateValid,
9 isVideoAbusePredefinedReasonsValid,
10 isVideoAbusePredefinedReasonValid,
11 isVideoAbuseTimestampValid,
12 isVideoAbuseTimestampCoherent
13} from '../../../helpers/custom-validators/video-abuses'
14import { logger } from '../../../helpers/logger'
15import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
16import { areValidationErrors } from '../utils'
17
18const videoAbuseReportValidator = [
19 param('videoId')
20 .custom(isIdOrUUIDValid)
21 .not()
22 .isEmpty()
23 .withMessage('Should have a valid videoId'),
24 body('reason')
25 .custom(isVideoAbuseReasonValid)
26 .withMessage('Should have a valid reason'),
27 body('predefinedReasons')
28 .optional()
29 .custom(isVideoAbusePredefinedReasonsValid)
30 .withMessage('Should have a valid list of predefined reasons'),
31 body('startAt')
32 .optional()
33 .customSanitizer(toIntOrNull)
34 .custom(isVideoAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'),
36 body('endAt')
37 .optional()
38 .customSanitizer(toIntOrNull)
39 .custom(isVideoAbuseTimestampValid)
40 .withMessage('Should have valid ending time value')
41 .bail()
42 .custom(isVideoAbuseTimestampCoherent)
43 .withMessage('Should have a startAt timestamp beginning before endAt'),
44
45 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
47
48 if (areValidationErrors(req, res)) return
49 if (!await doesVideoExist(req.params.videoId, res)) return
50
51 return next()
52 }
53]
54
55const videoAbuseGetValidator = [
56 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
57 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
58
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
61
62 if (areValidationErrors(req, res)) return
63 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
64
65 return next()
66 }
67]
68
69const videoAbuseUpdateValidator = [
70 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
71 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
72 body('state')
73 .optional()
74 .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
75 body('moderationComment')
76 .optional()
77 .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
78
79 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
80 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
81
82 if (areValidationErrors(req, res)) return
83 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
84
85 return next()
86 }
87]
88
89const videoAbuseListValidator = [
90 query('id')
91 .optional()
92 .custom(isIdValid).withMessage('Should have a valid id'),
93 query('predefinedReason')
94 .optional()
95 .custom(isVideoAbusePredefinedReasonValid)
96 .withMessage('Should have a valid predefinedReason'),
97 query('search')
98 .optional()
99 .custom(exists).withMessage('Should have a valid search'),
100 query('state')
101 .optional()
102 .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
103 query('videoIs')
104 .optional()
105 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
106 query('searchReporter')
107 .optional()
108 .custom(exists).withMessage('Should have a valid reporter search'),
109 query('searchReportee')
110 .optional()
111 .custom(exists).withMessage('Should have a valid reportee search'),
112 query('searchVideo')
113 .optional()
114 .custom(exists).withMessage('Should have a valid video search'),
115 query('searchVideoChannel')
116 .optional()
117 .custom(exists).withMessage('Should have a valid video channel search'),
118
119 (req: express.Request, res: express.Response, next: express.NextFunction) => {
120 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
121
122 if (areValidationErrors(req, res)) return
123
124 return next()
125 }
126]
127
128// ---------------------------------------------------------------------------
129
130export {
131 videoAbuseListValidator,
132 videoAbuseReportValidator,
133 videoAbuseGetValidator,
134 videoAbuseUpdateValidator
135}
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index ef019fcf9..77f5c6ff3 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -3,13 +3,16 @@ import { body, param } from 'express-validator'
3import { MUserAccountUrl } from '@server/types/models' 3import { MUserAccountUrl } from '@server/types/models'
4import { UserRight } from '../../../../shared' 4import { UserRight } from '../../../../shared'
5import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
6import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' 6import {
7 doesVideoCommentExist,
8 doesVideoCommentThreadExist,
9 isValidVideoCommentText
10} from '../../../helpers/custom-validators/video-comments'
7import { logger } from '../../../helpers/logger' 11import { logger } from '../../../helpers/logger'
8import { doesVideoExist } from '../../../helpers/middlewares' 12import { doesVideoExist } from '../../../helpers/middlewares'
9import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' 13import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
10import { Hooks } from '../../../lib/plugins/hooks' 14import { Hooks } from '../../../lib/plugins/hooks'
11import { VideoCommentModel } from '../../../models/video/video-comment' 15import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
12import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../types/models/video'
13import { areValidationErrors } from '../utils' 16import { areValidationErrors } from '../utils'
14 17
15const listVideoCommentThreadsValidator = [ 18const listVideoCommentThreadsValidator = [
@@ -120,67 +123,10 @@ export {
120 123
121// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
122 125
123async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
124 const id = parseInt(idArg + '', 10)
125 const videoComment = await VideoCommentModel.loadById(id)
126
127 if (!videoComment) {
128 res.status(404)
129 .json({ error: 'Video comment thread not found' })
130 .end()
131
132 return false
133 }
134
135 if (videoComment.videoId !== video.id) {
136 res.status(400)
137 .json({ error: 'Video comment is not associated to this video.' })
138 .end()
139
140 return false
141 }
142
143 if (videoComment.inReplyToCommentId !== null) {
144 res.status(400)
145 .json({ error: 'Video comment is not a thread.' })
146 .end()
147
148 return false
149 }
150
151 res.locals.videoCommentThread = videoComment
152 return true
153}
154
155async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
156 const id = parseInt(idArg + '', 10)
157 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
158
159 if (!videoComment) {
160 res.status(404)
161 .json({ error: 'Video comment thread not found' })
162 .end()
163
164 return false
165 }
166
167 if (videoComment.videoId !== video.id) {
168 res.status(400)
169 .json({ error: 'Video comment is not associated to this video.' })
170 .end()
171
172 return false
173 }
174
175 res.locals.videoCommentFull = videoComment
176 return true
177}
178
179function isVideoCommentsEnabled (video: MVideo, res: express.Response) { 126function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
180 if (video.commentsEnabled !== true) { 127 if (video.commentsEnabled !== true) {
181 res.status(409) 128 res.status(409)
182 .json({ error: 'Video comments are disabled for this video.' }) 129 .json({ error: 'Video comments are disabled for this video.' })
183 .end()
184 130
185 return false 131 return false
186 } 132 }
@@ -192,7 +138,7 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
192 if (videoComment.isDeleted()) { 138 if (videoComment.isDeleted()) {
193 res.status(409) 139 res.status(409)
194 .json({ error: 'This comment is already deleted' }) 140 .json({ error: 'This comment is already deleted' })
195 .end() 141
196 return false 142 return false
197 } 143 }
198 144
@@ -240,7 +186,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
240 if (!acceptedResult || acceptedResult.accepted !== true) { 186 if (!acceptedResult || acceptedResult.accepted !== true) {
241 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 187 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
242 res.status(403) 188 res.status(403)
243 .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) 189 .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
244 190
245 return false 191 return false
246 } 192 }
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts
new file mode 100644
index 000000000..5fddcf3c4
--- /dev/null
+++ b/server/models/abuse/abuse-query-builder.ts
@@ -0,0 +1,154 @@
1
2import { exists } from '@server/helpers/custom-validators/misc'
3import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models'
4import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils'
5
6export type BuildAbusesQueryOptions = {
7 start: number
8 count: number
9 sort: string
10
11 // search
12 search?: string
13 searchReporter?: string
14 searchReportee?: string
15
16 // video releated
17 searchVideo?: string
18 searchVideoChannel?: string
19 videoIs?: AbuseVideoIs
20
21 // filters
22 id?: number
23 predefinedReasonId?: number
24 filter?: AbuseFilter
25
26 state?: AbuseState
27
28 // accountIds
29 serverAccountId: number
30 userAccountId: number
31}
32
33function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') {
34 const whereAnd: string[] = []
35 const replacements: any = {}
36
37 const joins = [
38 'LEFT JOIN "videoAbuse" ON "videoAbuse"."abuseId" = "abuse"."id"',
39 'LEFT JOIN "video" ON "videoAbuse"."videoId" = "video"."id"',
40 'LEFT JOIN "videoBlacklist" ON "videoBlacklist"."videoId" = "video"."id"',
41 'LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"',
42 'LEFT JOIN "account" "reporterAccount" ON "reporterAccount"."id" = "abuse"."reporterAccountId"',
43 'LEFT JOIN "account" "flaggedAccount" ON "flaggedAccount"."id" = "abuse"."reporterAccountId"',
44 'LEFT JOIN "commentAbuse" ON "commentAbuse"."abuseId" = "abuse"."id"',
45 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"'
46 ]
47
48 whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
49
50 if (options.search) {
51 const searchWhereOr = [
52 '"video"."name" ILIKE :search',
53 '"videoChannel"."name" ILIKE :search',
54 `"videoAbuse"."deletedVideo"->>'name' ILIKE :search`,
55 `"videoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE :search`,
56 '"reporterAccount"."name" ILIKE :search',
57 '"flaggedAccount"."name" ILIKE :search'
58 ]
59
60 replacements.search = `%${options.search}%`
61 whereAnd.push('(' + searchWhereOr.join(' OR ') + ')')
62 }
63
64 if (options.searchVideo) {
65 whereAnd.push('"video"."name" ILIKE :searchVideo')
66 replacements.searchVideo = `%${options.searchVideo}%`
67 }
68
69 if (options.searchVideoChannel) {
70 whereAnd.push('"videoChannel"."name" ILIKE :searchVideoChannel')
71 replacements.searchVideoChannel = `%${options.searchVideoChannel}%`
72 }
73
74 if (options.id) {
75 whereAnd.push('"abuse"."id" = :id')
76 replacements.id = options.id
77 }
78
79 if (options.state) {
80 whereAnd.push('"abuse"."state" = :state')
81 replacements.state = options.state
82 }
83
84 if (options.videoIs === 'deleted') {
85 whereAnd.push('"videoAbuse"."deletedVideo" IS NOT NULL')
86 } else if (options.videoIs === 'blacklisted') {
87 whereAnd.push('"videoBlacklist"."id" IS NOT NULL')
88 }
89
90 if (options.predefinedReasonId) {
91 whereAnd.push(':predefinedReasonId = ANY("abuse"."predefinedReasons")')
92 replacements.predefinedReasonId = options.predefinedReasonId
93 }
94
95 if (options.filter === 'video') {
96 whereAnd.push('"videoAbuse"."id" IS NOT NULL')
97 } else if (options.filter === 'comment') {
98 whereAnd.push('"commentAbuse"."id" IS NOT NULL')
99 } else if (options.filter === 'account') {
100 whereAnd.push('"videoAbuse"."id" IS NULL AND "commentAbuse"."id" IS NULL')
101 }
102
103 if (options.searchReporter) {
104 whereAnd.push('"reporterAccount"."name" ILIKE :searchReporter')
105 replacements.searchReporter = `%${options.searchReporter}%`
106 }
107
108 if (options.searchReportee) {
109 whereAnd.push('"flaggedAccount"."name" ILIKE :searchReportee')
110 replacements.searchReportee = `%${options.searchReportee}%`
111 }
112
113 const prefix = type === 'count'
114 ? 'SELECT COUNT("abuse"."id") AS "total"'
115 : 'SELECT "abuse"."id" '
116
117 let suffix = ''
118 if (type !== 'count') {
119
120 if (options.sort) {
121 const order = buildAbuseOrder(options.sort)
122 suffix += `${order} `
123 }
124
125 if (exists(options.count)) {
126 const count = parseInt(options.count + '', 10)
127 suffix += `LIMIT ${count} `
128 }
129
130 if (exists(options.start)) {
131 const start = parseInt(options.start + '', 10)
132 suffix += `OFFSET ${start} `
133 }
134 }
135
136 const where = whereAnd.length !== 0
137 ? `WHERE ${whereAnd.join(' AND ')}`
138 : ''
139
140 return {
141 query: `${prefix} FROM "abuse" ${joins.join(' ')} ${where} ${suffix}`,
142 replacements
143 }
144}
145
146function buildAbuseOrder (value: string) {
147 const { direction, field } = buildDirectionAndField(value)
148
149 return `ORDER BY "abuse"."${field}" ${direction}`
150}
151
152export {
153 buildAbuseListQuery
154}
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
new file mode 100644
index 000000000..bd96cf79c
--- /dev/null
+++ b/server/models/abuse/abuse.ts
@@ -0,0 +1,515 @@
1import * as Bluebird from 'bluebird'
2import { invert } from 'lodash'
3import { literal, Op, QueryTypes, WhereOptions } from 'sequelize'
4import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasOne,
13 Is,
14 Model,
15 Scopes,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
20import {
21 Abuse,
22 AbuseFilter,
23 AbuseObject,
24 AbusePredefinedReasons,
25 abusePredefinedReasonsMap,
26 AbusePredefinedReasonsString,
27 AbuseState,
28 AbuseVideoIs,
29 VideoAbuse,
30 VideoCommentAbuse
31} from '@shared/models'
32import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
33import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
34import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
35import { getSort, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from '../video/thumbnail'
37import { VideoModel } from '../video/video'
38import { VideoBlacklistModel } from '../video/video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
40import { VideoCommentModel } from '../video/video-comment'
41import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder'
42import { VideoAbuseModel } from './video-abuse'
43import { VideoCommentAbuseModel } from './video-comment-abuse'
44
45export enum ScopeNames {
46 FOR_API = 'FOR_API'
47}
48
49@Scopes(() => ({
50 [ScopeNames.FOR_API]: () => {
51 return {
52 attributes: {
53 include: [
54 [
55 // we don't care about this count for deleted videos, so there are not included
56 literal(
57 '(' +
58 'SELECT count(*) ' +
59 'FROM "videoAbuse" ' +
60 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
61 ')'
62 ),
63 'countReportsForVideo'
64 ],
65 [
66 // we don't care about this count for deleted videos, so there are not included
67 literal(
68 '(' +
69 'SELECT t.nth ' +
70 'FROM ( ' +
71 'SELECT id, ' +
72 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
73 'FROM "videoAbuse" ' +
74 ') t ' +
75 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' +
76 ')'
77 ),
78 'nthReportForVideo'
79 ],
80 [
81 literal(
82 '(' +
83 'SELECT count("abuse"."id") ' +
84 'FROM "abuse" ' +
85 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
86 ')'
87 ),
88 'countReportsForReporter'
89 ],
90 [
91 literal(
92 '(' +
93 'SELECT count("abuse"."id") ' +
94 'FROM "abuse" ' +
95 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
96 ')'
97 ),
98 'countReportsForReportee'
99 ]
100 ]
101 },
102 include: [
103 {
104 model: AccountModel.scope({
105 method: [
106 AccountScopeNames.SUMMARY,
107 { actorRequired: false } as AccountSummaryOptions
108 ]
109 }),
110 as: 'ReporterAccount'
111 },
112 {
113 model: AccountModel.scope({
114 method: [
115 AccountScopeNames.SUMMARY,
116 { actorRequired: false } as AccountSummaryOptions
117 ]
118 }),
119 as: 'FlaggedAccount'
120 },
121 {
122 model: VideoCommentAbuseModel.unscoped(),
123 include: [
124 {
125 model: VideoCommentModel.unscoped(),
126 include: [
127 {
128 model: VideoModel.unscoped(),
129 attributes: [ 'name', 'id', 'uuid' ]
130 }
131 ]
132 }
133 ]
134 },
135 {
136 model: VideoAbuseModel.unscoped(),
137 include: [
138 {
139 attributes: [ 'id', 'uuid', 'name', 'nsfw' ],
140 model: VideoModel.unscoped(),
141 include: [
142 {
143 attributes: [ 'filename', 'fileUrl', 'type' ],
144 model: ThumbnailModel
145 },
146 {
147 model: VideoChannelModel.scope({
148 method: [
149 VideoChannelScopeNames.SUMMARY,
150 { withAccount: false, actorRequired: false } as ChannelSummaryOptions
151 ]
152 }),
153 required: false
154 },
155 {
156 attributes: [ 'id', 'reason', 'unfederated' ],
157 required: false,
158 model: VideoBlacklistModel
159 }
160 ]
161 }
162 ]
163 }
164 ]
165 }
166 }
167}))
168@Table({
169 tableName: 'abuse',
170 indexes: [
171 {
172 fields: [ 'reporterAccountId' ]
173 },
174 {
175 fields: [ 'flaggedAccountId' ]
176 }
177 ]
178})
179export class AbuseModel extends Model<AbuseModel> {
180
181 @AllowNull(false)
182 @Default(null)
183 @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
184 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
185 reason: string
186
187 @AllowNull(false)
188 @Default(null)
189 @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
190 @Column
191 state: AbuseState
192
193 @AllowNull(true)
194 @Default(null)
195 @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
196 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
197 moderationComment: string
198
199 @AllowNull(true)
200 @Default(null)
201 @Column(DataType.ARRAY(DataType.INTEGER))
202 predefinedReasons: AbusePredefinedReasons[]
203
204 @CreatedAt
205 createdAt: Date
206
207 @UpdatedAt
208 updatedAt: Date
209
210 @ForeignKey(() => AccountModel)
211 @Column
212 reporterAccountId: number
213
214 @BelongsTo(() => AccountModel, {
215 foreignKey: {
216 name: 'reporterAccountId',
217 allowNull: true
218 },
219 as: 'ReporterAccount',
220 onDelete: 'set null'
221 })
222 ReporterAccount: AccountModel
223
224 @ForeignKey(() => AccountModel)
225 @Column
226 flaggedAccountId: number
227
228 @BelongsTo(() => AccountModel, {
229 foreignKey: {
230 name: 'flaggedAccountId',
231 allowNull: true
232 },
233 as: 'FlaggedAccount',
234 onDelete: 'set null'
235 })
236 FlaggedAccount: AccountModel
237
238 @HasOne(() => VideoCommentAbuseModel, {
239 foreignKey: {
240 name: 'abuseId',
241 allowNull: false
242 },
243 onDelete: 'cascade'
244 })
245 VideoCommentAbuse: VideoCommentAbuseModel
246
247 @HasOne(() => VideoAbuseModel, {
248 foreignKey: {
249 name: 'abuseId',
250 allowNull: false
251 },
252 onDelete: 'cascade'
253 })
254 VideoAbuse: VideoAbuseModel
255
256 // FIXME: deprecated in 2.3. Remove these validators
257 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
258 const videoWhere: WhereOptions = {}
259
260 if (videoId) videoWhere.videoId = videoId
261 if (uuid) videoWhere.deletedVideo = { uuid }
262
263 const query = {
264 include: [
265 {
266 model: VideoAbuseModel,
267 required: true,
268 where: videoWhere
269 }
270 ],
271 where: {
272 id
273 }
274 }
275 return AbuseModel.findOne(query)
276 }
277
278 static loadById (id: number): Bluebird<MAbuse> {
279 const query = {
280 where: {
281 id
282 }
283 }
284
285 return AbuseModel.findOne(query)
286 }
287
288 static async listForApi (parameters: {
289 start: number
290 count: number
291 sort: string
292
293 filter?: AbuseFilter
294
295 serverAccountId: number
296 user?: MUserAccountId
297
298 id?: number
299 predefinedReason?: AbusePredefinedReasonsString
300 state?: AbuseState
301 videoIs?: AbuseVideoIs
302
303 search?: string
304 searchReporter?: string
305 searchReportee?: string
306 searchVideo?: string
307 searchVideoChannel?: string
308 }) {
309 const {
310 start,
311 count,
312 sort,
313 search,
314 user,
315 serverAccountId,
316 state,
317 videoIs,
318 predefinedReason,
319 searchReportee,
320 searchVideo,
321 filter,
322 searchVideoChannel,
323 searchReporter,
324 id
325 } = parameters
326
327 const userAccountId = user ? user.Account.id : undefined
328 const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
329
330 const queryOptions: BuildAbusesQueryOptions = {
331 start,
332 count,
333 sort,
334 id,
335 filter,
336 predefinedReasonId,
337 search,
338 state,
339 videoIs,
340 searchReportee,
341 searchVideo,
342 searchVideoChannel,
343 searchReporter,
344 serverAccountId,
345 userAccountId
346 }
347
348 const [ total, data ] = await Promise.all([
349 AbuseModel.internalCountForApi(queryOptions),
350 AbuseModel.internalListForApi(queryOptions)
351 ])
352
353 return { total, data }
354 }
355
356 toFormattedJSON (this: MAbuseFormattable): Abuse {
357 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
358
359 const countReportsForVideo = this.get('countReportsForVideo') as number
360 const nthReportForVideo = this.get('nthReportForVideo') as number
361
362 const countReportsForReporter = this.get('countReportsForReporter') as number
363 const countReportsForReportee = this.get('countReportsForReportee') as number
364
365 let video: VideoAbuse = null
366 let comment: VideoCommentAbuse = null
367
368 if (this.VideoAbuse) {
369 const abuseModel = this.VideoAbuse
370 const entity = abuseModel.Video || abuseModel.deletedVideo
371
372 video = {
373 id: entity.id,
374 uuid: entity.uuid,
375 name: entity.name,
376 nsfw: entity.nsfw,
377
378 startAt: abuseModel.startAt,
379 endAt: abuseModel.endAt,
380
381 deleted: !abuseModel.Video,
382 blacklisted: abuseModel.Video?.isBlacklisted() || false,
383 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
384
385 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
386
387 countReports: countReportsForVideo,
388 nthReport: nthReportForVideo
389 }
390 }
391
392 if (this.VideoCommentAbuse) {
393 const abuseModel = this.VideoCommentAbuse
394 const entity = abuseModel.VideoComment
395
396 comment = {
397 id: entity.id,
398 threadId: entity.getThreadId(),
399
400 text: entity.text ?? '',
401
402 deleted: entity.isDeleted(),
403
404 video: {
405 id: entity.Video.id,
406 name: entity.Video.name,
407 uuid: entity.Video.uuid
408 }
409 }
410 }
411
412 return {
413 id: this.id,
414 reason: this.reason,
415 predefinedReasons,
416
417 reporterAccount: this.ReporterAccount
418 ? this.ReporterAccount.toFormattedJSON()
419 : null,
420
421 flaggedAccount: this.FlaggedAccount
422 ? this.FlaggedAccount.toFormattedJSON()
423 : null,
424
425 state: {
426 id: this.state,
427 label: AbuseModel.getStateLabel(this.state)
428 },
429
430 moderationComment: this.moderationComment,
431
432 video,
433 comment,
434
435 createdAt: this.createdAt,
436 updatedAt: this.updatedAt,
437
438 countReportsForReporter: (countReportsForReporter || 0),
439 countReportsForReportee: (countReportsForReportee || 0),
440
441 // FIXME: deprecated in 2.3, remove this
442 startAt: null,
443 endAt: null,
444 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0
446 }
447 }
448
449 toActivityPubObject (this: MAbuseAP): AbuseObject {
450 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
451
452 const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
453
454 const startAt = this.VideoAbuse?.startAt
455 const endAt = this.VideoAbuse?.endAt
456
457 return {
458 type: 'Flag' as 'Flag',
459 content: this.reason,
460 object,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
467 }
468 }
469
470 private static async internalCountForApi (parameters: BuildAbusesQueryOptions) {
471 const { query, replacements } = buildAbuseListQuery(parameters, 'count')
472 const options = {
473 type: QueryTypes.SELECT as QueryTypes.SELECT,
474 replacements
475 }
476
477 const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options)
478 if (total === null) return 0
479
480 return parseInt(total, 10)
481 }
482
483 private static async internalListForApi (parameters: BuildAbusesQueryOptions) {
484 const { query, replacements } = buildAbuseListQuery(parameters, 'id')
485 const options = {
486 type: QueryTypes.SELECT as QueryTypes.SELECT,
487 replacements
488 }
489
490 const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options)
491 const ids = rows.map(r => r.id)
492
493 if (ids.length === 0) return []
494
495 return AbuseModel.scope(ScopeNames.FOR_API)
496 .findAll({
497 order: getSort(parameters.sort),
498 where: {
499 id: {
500 [Op.in]: ids
501 }
502 }
503 })
504 }
505
506 private static getStateLabel (id: number) {
507 return ABUSE_STATES[id] || 'Unknown'
508 }
509
510 private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
511 return (predefinedReasons || [])
512 .filter(r => r in AbusePredefinedReasons)
513 .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
514 }
515}
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts
new file mode 100644
index 000000000..d92bcf19f
--- /dev/null
+++ b/server/models/abuse/video-abuse.ts
@@ -0,0 +1,63 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoDetails } from '@shared/models'
3import { VideoModel } from '../video/video'
4import { AbuseModel } from './abuse'
5
6@Table({
7 tableName: 'videoAbuse',
8 indexes: [
9 {
10 fields: [ 'abuseId' ]
11 },
12 {
13 fields: [ 'videoId' ]
14 }
15 ]
16})
17export class VideoAbuseModel extends Model<VideoAbuseModel> {
18
19 @CreatedAt
20 createdAt: Date
21
22 @UpdatedAt
23 updatedAt: Date
24
25 @AllowNull(true)
26 @Default(null)
27 @Column
28 startAt: number
29
30 @AllowNull(true)
31 @Default(null)
32 @Column
33 endAt: number
34
35 @AllowNull(true)
36 @Default(null)
37 @Column(DataType.JSONB)
38 deletedVideo: VideoDetails
39
40 @ForeignKey(() => AbuseModel)
41 @Column
42 abuseId: number
43
44 @BelongsTo(() => AbuseModel, {
45 foreignKey: {
46 allowNull: false
47 },
48 onDelete: 'cascade'
49 })
50 Abuse: AbuseModel
51
52 @ForeignKey(() => VideoModel)
53 @Column
54 videoId: number
55
56 @BelongsTo(() => VideoModel, {
57 foreignKey: {
58 allowNull: true
59 },
60 onDelete: 'set null'
61 })
62 Video: VideoModel
63}
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts
new file mode 100644
index 000000000..8b34009b4
--- /dev/null
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -0,0 +1,47 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoCommentModel } from '../video/video-comment'
3import { AbuseModel } from './abuse'
4
5@Table({
6 tableName: 'commentAbuse',
7 indexes: [
8 {
9 fields: [ 'abuseId' ]
10 },
11 {
12 fields: [ 'videoCommentId' ]
13 }
14 ]
15})
16export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
17
18 @CreatedAt
19 createdAt: Date
20
21 @UpdatedAt
22 updatedAt: Date
23
24 @ForeignKey(() => AbuseModel)
25 @Column
26 abuseId: number
27
28 @BelongsTo(() => AbuseModel, {
29 foreignKey: {
30 allowNull: false
31 },
32 onDelete: 'cascade'
33 })
34 Abuse: AbuseModel
35
36 @ForeignKey(() => VideoCommentModel)
37 @Column
38 videoCommentId: number
39
40 @BelongsTo(() => VideoCommentModel, {
41 foreignKey: {
42 allowNull: true
43 },
44 onDelete: 'set null'
45 })
46 VideoComment: VideoCommentModel
47}
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index cf8872fd5..577b7dc19 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -1,12 +1,12 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort, searchAttribute } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize'
6import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
7import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
5import { AccountBlock } from '../../../shared/models'
8import { ActorModel } from '../activitypub/actor' 6import { ActorModel } from '../activitypub/actor'
9import { ServerModel } from '../server/server' 7import { ServerModel } from '../server/server'
8import { getSort, searchAttribute } from '../utils'
9import { AccountModel } from './account'
10 10
11enum ScopeNames { 11enum ScopeNames {
12 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 12 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 4395d179a..f97519b14 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -42,6 +42,7 @@ export enum ScopeNames {
42} 42}
43 43
44export type SummaryOptions = { 44export type SummaryOptions = {
45 actorRequired?: boolean // Default: true
45 whereActor?: WhereOptions 46 whereActor?: WhereOptions
46 withAccountBlockerIds?: number[] 47 withAccountBlockerIds?: number[]
47} 48}
@@ -65,12 +66,12 @@ export type SummaryOptions = {
65 } 66 }
66 67
67 const query: FindOptions = { 68 const query: FindOptions = {
68 attributes: [ 'id', 'name' ], 69 attributes: [ 'id', 'name', 'actorId' ],
69 include: [ 70 include: [
70 { 71 {
71 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 72 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
72 model: ActorModel.unscoped(), 73 model: ActorModel.unscoped(),
73 required: true, 74 required: options.actorRequired ?? true,
74 where: whereActor, 75 where: whereActor,
75 include: [ 76 include: [
76 serverInclude, 77 serverInclude,
@@ -388,6 +389,10 @@ export class AccountModel extends Model<AccountModel> {
388 .findAll(query) 389 .findAll(query)
389 } 390 }
390 391
392 getClientUrl () {
393 return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
394 }
395
391 toFormattedJSON (this: MAccountFormattable): Account { 396 toFormattedJSON (this: MAccountFormattable): Account {
392 const actor = this.Actor.toFormattedJSON() 397 const actor = this.Actor.toFormattedJSON()
393 const account = { 398 const account = {
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index b69b47265..d8f3f13da 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
51 @AllowNull(false) 51 @AllowNull(false)
52 @Default(null) 52 @Default(null)
53 @Is( 53 @Is(
54 'UserNotificationSettingVideoAbuseAsModerator', 54 'UserNotificationSettingAbuseAsModerator',
55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') 55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
56 ) 56 )
57 @Column 57 @Column
58 videoAbuseAsModerator: UserNotificationSettingValue 58 abuseAsModerator: UserNotificationSettingValue
59 59
60 @AllowNull(false) 60 @AllowNull(false)
61 @Default(null) 61 @Default(null)
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
166 return { 166 return {
167 newCommentOnMyVideo: this.newCommentOnMyVideo, 167 newCommentOnMyVideo: this.newCommentOnMyVideo,
168 newVideoFromSubscription: this.newVideoFromSubscription, 168 newVideoFromSubscription: this.newVideoFromSubscription,
169 videoAbuseAsModerator: this.videoAbuseAsModerator, 169 abuseAsModerator: this.abuseAsModerator,
170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, 170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
171 blacklistOnMyVideo: this.blacklistOnMyVideo, 171 blacklistOnMyVideo: this.blacklistOnMyVideo,
172 myVideoPublished: this.myVideoPublished, 172 myVideoPublished: this.myVideoPublished,
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 30985bb0f..2945bf709 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -1,22 +1,24 @@
1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
2import { UserNotification, UserNotificationType } from '../../../shared' 4import { UserNotification, UserNotificationType } from '../../../shared'
3import { getSort, throwIfNotValid } from '../utils'
4import { isBooleanValid } from '../../helpers/custom-validators/misc' 5import { isBooleanValid } from '../../helpers/custom-validators/misc'
5import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' 6import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
6import { UserModel } from './user' 7import { AbuseModel } from '../abuse/abuse'
7import { VideoModel } from '../video/video' 8import { VideoAbuseModel } from '../abuse/video-abuse'
8import { VideoCommentModel } from '../video/video-comment' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10import { VideoChannelModel } from '../video/video-channel'
11import { AccountModel } from './account'
12import { VideoAbuseModel } from '../video/video-abuse'
13import { VideoBlacklistModel } from '../video/video-blacklist'
14import { VideoImportModel } from '../video/video-import'
15import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
16import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
17import { AvatarModel } from '../avatar/avatar' 12import { AvatarModel } from '../avatar/avatar'
18import { ServerModel } from '../server/server' 13import { ServerModel } from '../server/server'
19import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' 14import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video'
16import { VideoBlacklistModel } from '../video/video-blacklist'
17import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account'
21import { UserModel } from './user'
20 22
21enum ScopeNames { 23enum ScopeNames {
22 WITH_ALL = 'WITH_ALL' 24 WITH_ALL = 'WITH_ALL'
@@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) {
87 89
88 { 90 {
89 attributes: [ 'id' ], 91 attributes: [ 'id' ],
90 model: VideoAbuseModel.unscoped(), 92 model: AbuseModel.unscoped(),
91 required: false, 93 required: false,
92 include: [ buildVideoInclude(true) ] 94 include: [
95 {
96 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(),
98 required: false,
99 include: [ buildVideoInclude(true) ]
100 },
101 {
102 attributes: [ 'id' ],
103 model: VideoCommentAbuseModel.unscoped(),
104 required: false,
105 include: [
106 {
107 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel,
109 required: true,
110 include: [
111 {
112 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(),
114 required: true
115 }
116 ]
117 }
118 ]
119 },
120 {
121 model: AccountModel,
122 as: 'FlaggedAccount',
123 required: true,
124 include: [ buildActorWithAvatarInclude() ]
125 }
126 ]
93 }, 127 },
94 128
95 { 129 {
@@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
179 } 213 }
180 }, 214 },
181 { 215 {
182 fields: [ 'videoAbuseId' ], 216 fields: [ 'abuseId' ],
183 where: { 217 where: {
184 videoAbuseId: { 218 abuseId: {
185 [Op.ne]: null 219 [Op.ne]: null
186 } 220 }
187 } 221 }
@@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
276 }) 310 })
277 Comment: VideoCommentModel 311 Comment: VideoCommentModel
278 312
279 @ForeignKey(() => VideoAbuseModel) 313 @ForeignKey(() => AbuseModel)
280 @Column 314 @Column
281 videoAbuseId: number 315 abuseId: number
282 316
283 @BelongsTo(() => VideoAbuseModel, { 317 @BelongsTo(() => AbuseModel, {
284 foreignKey: { 318 foreignKey: {
285 allowNull: true 319 allowNull: true
286 }, 320 },
287 onDelete: 'cascade' 321 onDelete: 'cascade'
288 }) 322 })
289 VideoAbuse: VideoAbuseModel 323 Abuse: AbuseModel
290 324
291 @ForeignKey(() => VideoBlacklistModel) 325 @ForeignKey(() => VideoBlacklistModel)
292 @Column 326 @Column
@@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
397 video: this.formatVideo(this.Comment.Video) 431 video: this.formatVideo(this.Comment.Video)
398 } : undefined 432 } : undefined
399 433
400 const videoAbuse = this.VideoAbuse ? { 434 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
401 id: this.VideoAbuse.id,
402 video: this.formatVideo(this.VideoAbuse.Video)
403 } : undefined
404 435
405 const videoBlacklist = this.VideoBlacklist ? { 436 const videoBlacklist = this.VideoBlacklist ? {
406 id: this.VideoBlacklist.id, 437 id: this.VideoBlacklist.id,
@@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
439 video, 470 video,
440 videoImport, 471 videoImport,
441 comment, 472 comment,
442 videoAbuse, 473 abuse,
443 videoBlacklist, 474 videoBlacklist,
444 account, 475 account,
445 actorFollow, 476 actorFollow,
@@ -456,6 +487,29 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
456 } 487 }
457 } 488 }
458 489
490 formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
491 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? {
492 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
493
494 video: {
495 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
496 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
497 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
498 }
499 } : undefined
500
501 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
502
503 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
504
505 return {
506 id: abuse.id,
507 video: videoAbuse,
508 comment: commentAbuse,
509 account: accountAbuse
510 }
511 }
512
459 formatActor ( 513 formatActor (
460 this: UserNotificationModelForApi, 514 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor 515 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index de193131a..5f45f8e7c 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' 22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -168,28 +168,26 @@ enum ScopeNames {
168 '(' + 168 '(' +
169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
170 'FROM (' + 170 'FROM (' +
171 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + 171 'SELECT COUNT("abuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 172 `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' + 173 'FROM "abuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 174 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177 'WHERE "account"."userId" = "UserModel"."id"' + 175 'WHERE "account"."userId" = "UserModel"."id"' +
178 ') t' + 176 ') t' +
179 ')' 177 ')'
180 ), 178 ),
181 'videoAbusesCount' 179 'abusesCount'
182 ], 180 ],
183 [ 181 [
184 literal( 182 literal(
185 '(' + 183 '(' +
186 'SELECT COUNT("videoAbuse"."id") ' + 184 'SELECT COUNT("abuse"."id") ' +
187 'FROM "videoAbuse" ' + 185 'FROM "abuse" ' +
188 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + 186 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
189 'WHERE "account"."userId" = "UserModel"."id"' + 187 'WHERE "account"."userId" = "UserModel"."id"' +
190 ')' 188 ')'
191 ), 189 ),
192 'videoAbusesCreatedCount' 190 'abusesCreatedCount'
193 ], 191 ],
194 [ 192 [
195 literal( 193 literal(
@@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> {
780 const videoQuotaUsed = this.get('videoQuotaUsed') 778 const videoQuotaUsed = this.get('videoQuotaUsed')
781 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 779 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
782 const videosCount = this.get('videosCount') 780 const videosCount = this.get('videosCount')
783 const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') 781 const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
784 const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') 782 const abusesCreatedCount = this.get('abusesCreatedCount')
785 const videoCommentsCount = this.get('videoCommentsCount') 783 const videoCommentsCount = this.get('videoCommentsCount')
786 784
787 const json: User = { 785 const json: User = {
@@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> {
815 videosCount: videosCount !== undefined 813 videosCount: videosCount !== undefined
816 ? parseInt(videosCount + '', 10) 814 ? parseInt(videosCount + '', 10)
817 : undefined, 815 : undefined,
818 videoAbusesCount: videoAbusesCount 816 abusesCount: abusesCount
819 ? parseInt(videoAbusesCount, 10) 817 ? parseInt(abusesCount, 10)
820 : undefined, 818 : undefined,
821 videoAbusesAcceptedCount: videoAbusesAcceptedCount 819 abusesAcceptedCount: abusesAcceptedCount
822 ? parseInt(videoAbusesAcceptedCount, 10) 820 ? parseInt(abusesAcceptedCount, 10)
823 : undefined, 821 : undefined,
824 videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined 822 abusesCreatedCount: abusesCreatedCount !== undefined
825 ? parseInt(videoAbusesCreatedCount + '', 10) 823 ? parseInt(abusesCreatedCount + '', 10)
826 : undefined, 824 : undefined,
827 videoCommentsCount: videoCommentsCount !== undefined 825 videoCommentsCount: videoCommentsCount !== undefined
828 ? parseInt(videoCommentsCount + '', 10) 826 ? parseInt(videoCommentsCount + '', 10)
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 30f0525e5..68cd72ee7 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -1,11 +1,11 @@
1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
4import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
5import { ServerBlock } from '@shared/models'
2import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort, searchAttribute } from '../utils' 7import { getSort, searchAttribute } from '../utils'
6import * as Bluebird from 'bluebird' 8import { ServerModel } from './server'
7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
8import { Op } from 'sequelize'
9 9
10enum ScopeNames { 10enum ScopeNames {
11 WITH_ACCOUNT = 'WITH_ACCOUNT', 11 WITH_ACCOUNT = 'WITH_ACCOUNT',
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
deleted file mode 100644
index 1319332f0..000000000
--- a/server/models/video/video-abuse.ts
+++ /dev/null
@@ -1,479 +0,0 @@
1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
3import {
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
14 Table,
15 UpdatedAt
16} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import {
19 VideoAbuseState,
20 VideoDetails,
21 VideoAbusePredefinedReasons,
22 VideoAbusePredefinedReasonsString,
23 videoAbusePredefinedReasonsMap
24} from '../../../shared'
25import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
26import { VideoAbuse } from '../../../shared/models/videos'
27import {
28 isVideoAbuseModerationCommentValid,
29 isVideoAbuseReasonValid,
30 isVideoAbuseStateValid
31} from '../../helpers/custom-validators/video-abuses'
32import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
33import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
34import { AccountModel } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from './thumbnail'
37import { VideoModel } from './video'
38import { VideoBlacklistModel } from './video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
40import { invert } from 'lodash'
41
42export enum ScopeNames {
43 FOR_API = 'FOR_API'
44}
45
46@Scopes(() => ({
47 [ScopeNames.FOR_API]: (options: {
48 // search
49 search?: string
50 searchReporter?: string
51 searchReportee?: string
52 searchVideo?: string
53 searchVideoChannel?: string
54
55 // filters
56 id?: number
57 predefinedReasonId?: number
58
59 state?: VideoAbuseState
60 videoIs?: VideoAbuseVideoIs
61
62 // accountIds
63 serverAccountId: number
64 userAccountId: number
65 }) => {
66 const where = {
67 reporterAccountId: {
68 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
69 }
70 }
71
72 if (options.search) {
73 Object.assign(where, {
74 [Op.or]: [
75 {
76 [Op.and]: [
77 { videoId: { [Op.not]: null } },
78 searchAttribute(options.search, '$Video.name$')
79 ]
80 },
81 {
82 [Op.and]: [
83 { videoId: { [Op.not]: null } },
84 searchAttribute(options.search, '$Video.VideoChannel.name$')
85 ]
86 },
87 {
88 [Op.and]: [
89 { deletedVideo: { [Op.not]: null } },
90 { deletedVideo: searchAttribute(options.search, 'name') }
91 ]
92 },
93 {
94 [Op.and]: [
95 { deletedVideo: { [Op.not]: null } },
96 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
97 ]
98 },
99 searchAttribute(options.search, '$Account.name$')
100 ]
101 })
102 }
103
104 if (options.id) Object.assign(where, { id: options.id })
105 if (options.state) Object.assign(where, { state: options.state })
106
107 if (options.videoIs === 'deleted') {
108 Object.assign(where, {
109 deletedVideo: {
110 [Op.not]: null
111 }
112 })
113 }
114
115 if (options.predefinedReasonId) {
116 Object.assign(where, {
117 predefinedReasons: {
118 [Op.contains]: [ options.predefinedReasonId ]
119 }
120 })
121 }
122
123 const onlyBlacklisted = options.videoIs === 'blacklisted'
124
125 return {
126 attributes: {
127 include: [
128 [
129 // we don't care about this count for deleted videos, so there are not included
130 literal(
131 '(' +
132 'SELECT count(*) ' +
133 'FROM "videoAbuse" ' +
134 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
135 ')'
136 ),
137 'countReportsForVideo'
138 ],
139 [
140 // we don't care about this count for deleted videos, so there are not included
141 literal(
142 '(' +
143 'SELECT t.nth ' +
144 'FROM ( ' +
145 'SELECT id, ' +
146 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
147 'FROM "videoAbuse" ' +
148 ') t ' +
149 'WHERE t.id = "VideoAbuseModel".id ' +
150 ')'
151 ),
152 'nthReportForVideo'
153 ],
154 [
155 literal(
156 '(' +
157 'SELECT count("videoAbuse"."id") ' +
158 'FROM "videoAbuse" ' +
159 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
160 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
161 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
162 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
163 ')'
164 ),
165 'countReportsForReporter__video'
166 ],
167 [
168 literal(
169 '(' +
170 'SELECT count(DISTINCT "videoAbuse"."id") ' +
171 'FROM "videoAbuse" ' +
172 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
173 ')'
174 ),
175 'countReportsForReporter__deletedVideo'
176 ],
177 [
178 literal(
179 '(' +
180 'SELECT count(DISTINCT "videoAbuse"."id") ' +
181 'FROM "videoAbuse" ' +
182 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
183 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
184 'INNER JOIN "account" ON ' +
185 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
186 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
187 ')'
188 ),
189 'countReportsForReportee__video'
190 ],
191 [
192 literal(
193 '(' +
194 'SELECT count(DISTINCT "videoAbuse"."id") ' +
195 'FROM "videoAbuse" ' +
196 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
197 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
198 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
199 ')'
200 ),
201 'countReportsForReportee__deletedVideo'
202 ]
203 ]
204 },
205 include: [
206 {
207 model: AccountModel,
208 required: true,
209 where: searchAttribute(options.searchReporter, 'name')
210 },
211 {
212 model: VideoModel,
213 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
214 where: searchAttribute(options.searchVideo, 'name'),
215 include: [
216 {
217 model: ThumbnailModel
218 },
219 {
220 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
221 where: searchAttribute(options.searchVideoChannel, 'name'),
222 include: [
223 {
224 model: AccountModel,
225 where: searchAttribute(options.searchReportee, 'name')
226 }
227 ]
228 },
229 {
230 attributes: [ 'id', 'reason', 'unfederated' ],
231 model: VideoBlacklistModel,
232 required: onlyBlacklisted
233 }
234 ]
235 }
236 ],
237 where
238 }
239 }
240}))
241@Table({
242 tableName: 'videoAbuse',
243 indexes: [
244 {
245 fields: [ 'videoId' ]
246 },
247 {
248 fields: [ 'reporterAccountId' ]
249 }
250 ]
251})
252export class VideoAbuseModel extends Model<VideoAbuseModel> {
253
254 @AllowNull(false)
255 @Default(null)
256 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
257 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
258 reason: string
259
260 @AllowNull(false)
261 @Default(null)
262 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
263 @Column
264 state: VideoAbuseState
265
266 @AllowNull(true)
267 @Default(null)
268 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
269 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
270 moderationComment: string
271
272 @AllowNull(true)
273 @Default(null)
274 @Column(DataType.JSONB)
275 deletedVideo: VideoDetails
276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[]
281
282 @AllowNull(true)
283 @Default(null)
284 @Column
285 startAt: number
286
287 @AllowNull(true)
288 @Default(null)
289 @Column
290 endAt: number
291
292 @CreatedAt
293 createdAt: Date
294
295 @UpdatedAt
296 updatedAt: Date
297
298 @ForeignKey(() => AccountModel)
299 @Column
300 reporterAccountId: number
301
302 @BelongsTo(() => AccountModel, {
303 foreignKey: {
304 allowNull: true
305 },
306 onDelete: 'set null'
307 })
308 Account: AccountModel
309
310 @ForeignKey(() => VideoModel)
311 @Column
312 videoId: number
313
314 @BelongsTo(() => VideoModel, {
315 foreignKey: {
316 allowNull: true
317 },
318 onDelete: 'set null'
319 })
320 Video: VideoModel
321
322 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
323 const videoAttributes = {}
324 if (videoId) videoAttributes['videoId'] = videoId
325 if (uuid) videoAttributes['deletedVideo'] = { uuid }
326
327 const query = {
328 where: {
329 id,
330 ...videoAttributes
331 }
332 }
333 return VideoAbuseModel.findOne(query)
334 }
335
336 static listForApi (parameters: {
337 start: number
338 count: number
339 sort: string
340
341 serverAccountId: number
342 user?: MUserAccountId
343
344 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString
346 state?: VideoAbuseState
347 videoIs?: VideoAbuseVideoIs
348
349 search?: string
350 searchReporter?: string
351 searchReportee?: string
352 searchVideo?: string
353 searchVideoChannel?: string
354 }) {
355 const {
356 start,
357 count,
358 sort,
359 search,
360 user,
361 serverAccountId,
362 state,
363 videoIs,
364 predefinedReason,
365 searchReportee,
366 searchVideo,
367 searchVideoChannel,
368 searchReporter,
369 id
370 } = parameters
371
372 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
374
375 const query = {
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 col: 'VideoAbuseModel.id',
380 distinct: true
381 }
382
383 const filters = {
384 id,
385 predefinedReasonId,
386 search,
387 state,
388 videoIs,
389 searchReportee,
390 searchVideo,
391 searchVideoChannel,
392 searchReporter,
393 serverAccountId,
394 userAccountId
395 }
396
397 return VideoAbuseModel
398 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] }
400 ])
401 .findAndCountAll(query)
402 .then(({ rows, count }) => {
403 return { total: count, data: rows }
404 })
405 }
406
407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
409 const countReportsForVideo = this.get('countReportsForVideo') as number
410 const nthReportForVideo = this.get('nthReportForVideo') as number
411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
412 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
413 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
414 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
415
416 const video = this.Video
417 ? this.Video
418 : this.deletedVideo
419
420 return {
421 id: this.id,
422 reason: this.reason,
423 predefinedReasons,
424 reporterAccount: this.Account.toFormattedJSON(),
425 state: {
426 id: this.state,
427 label: VideoAbuseModel.getStateLabel(this.state)
428 },
429 moderationComment: this.moderationComment,
430 video: {
431 id: video.id,
432 uuid: video.uuid,
433 name: video.name,
434 nsfw: video.nsfw,
435 deleted: !this.Video,
436 blacklisted: this.Video?.isBlacklisted() || false,
437 thumbnailPath: this.Video?.getMiniatureStaticPath(),
438 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
439 },
440 createdAt: this.createdAt,
441 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
444 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0,
446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
447 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
448 }
449 }
450
451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
453
454 const startAt = this.startAt
455 const endAt = this.endAt
456
457 return {
458 type: 'Flag' as 'Flag',
459 content: this.reason,
460 object: this.Video.url,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
467 }
468 }
469
470 private static getStateLabel (id: number) {
471 return VIDEO_ABUSE_STATES[id] || 'Unknown'
472 }
473
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
475 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
478 }
479}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9cee64229..03a3cdf81 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -61,6 +61,7 @@ type AvailableWithStatsOptions = {
61} 61}
62 62
63export type SummaryOptions = { 63export type SummaryOptions = {
64 actorRequired?: boolean // Default: true
64 withAccount?: boolean // Default: false 65 withAccount?: boolean // Default: false
65 withAccountBlockerIds?: number[] 66 withAccountBlockerIds?: number[]
66} 67}
@@ -121,7 +122,7 @@ export type SummaryOptions = {
121 { 122 {
122 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 123 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
123 model: ActorModel.unscoped(), 124 model: ActorModel.unscoped(),
124 required: true, 125 required: options.actorRequired ?? true,
125 include: [ 126 include: [
126 { 127 {
127 attributes: [ 'host' ], 128 attributes: [ 'host' ],
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 90625d987..75b914b8c 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,7 +1,20 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash' 2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' 3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 4import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 ForeignKey,
11 HasMany,
12 Is,
13 Model,
14 Scopes,
15 Table,
16 UpdatedAt
17} from 'sequelize-typescript'
5import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
6import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 19import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
7import { VideoPrivacy } from '@shared/models' 20import { VideoPrivacy } from '@shared/models'
@@ -24,6 +37,7 @@ import {
24 MCommentOwnerVideoReply, 37 MCommentOwnerVideoReply,
25 MVideoImmutable 38 MVideoImmutable
26} from '../../types/models/video' 39} from '../../types/models/video'
40import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
27import { AccountModel } from '../account/account' 41import { AccountModel } from '../account/account'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 42import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' 43import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
@@ -224,6 +238,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
224 }) 238 })
225 Account: AccountModel 239 Account: AccountModel
226 240
241 @HasMany(() => VideoCommentAbuseModel, {
242 foreignKey: {
243 name: 'videoCommentId',
244 allowNull: true
245 },
246 onDelete: 'set null'
247 })
248 CommentAbuses: VideoCommentAbuseModel[]
249
227 static loadById (id: number, t?: Transaction): Bluebird<MComment> { 250 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
228 const query: FindOptions = { 251 const query: FindOptions = {
229 where: { 252 where: {
@@ -632,7 +655,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
632 id: this.id, 655 id: this.id,
633 url: this.url, 656 url: this.url,
634 text: this.text, 657 text: this.text,
635 threadId: this.originCommentId || this.id, 658 threadId: this.getThreadId(),
636 inReplyToCommentId: this.inReplyToCommentId || null, 659 inReplyToCommentId: this.inReplyToCommentId || null,
637 videoId: this.videoId, 660 videoId: this.videoId,
638 createdAt: this.createdAt, 661 createdAt: this.createdAt,
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 984b0e6af..466890364 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -327,7 +327,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
327 attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') 327 attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"')
328 } 328 }
329 329
330 order = buildOrder(model, options.sort) 330 order = buildOrder(options.sort)
331 suffix += `${order} ` 331 suffix += `${order} `
332 } 332 }
333 333
@@ -357,7 +357,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
357 return { query, replacements, order } 357 return { query, replacements, order }
358} 358}
359 359
360function buildOrder (model: typeof Model, value: string) { 360function buildOrder (value: string) {
361 const { direction, field } = buildDirectionAndField(value) 361 const { direction, field } = buildDirectionAndField(value)
362 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) 362 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field)
363 363
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e2718300e..43609587c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,4 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { remove } from 'fs-extra'
2import { maxBy, minBy, pick } from 'lodash' 3import { maxBy, minBy, pick } from 'lodash'
3import { join } from 'path' 4import { join } from 'path'
4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 5import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
@@ -23,10 +24,18 @@ import {
23 Table, 24 Table,
24 UpdatedAt 25 UpdatedAt
25} from 'sequelize-typescript' 26} from 'sequelize-typescript'
26import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
29import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
30import { getServerActor } from '@server/models/application/application'
31import { ModelCache } from '@server/models/model-cache'
32import { VideoFile } from '@shared/models/videos/video-file.model'
33import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
27import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 34import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
28import { Video, VideoDetails } from '../../../shared/models/videos' 35import { Video, VideoDetails } from '../../../shared/models/videos'
36import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
29import { VideoFilter } from '../../../shared/models/videos/video-query.type' 37import { VideoFilter } from '../../../shared/models/videos/video-query.type'
38import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
30import { peertubeTruncate } from '../../helpers/core-utils' 39import { peertubeTruncate } from '../../helpers/core-utils'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 40import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { isBooleanValid } from '../../helpers/custom-validators/misc' 41import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -43,6 +52,7 @@ import {
43} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 53import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
45import { logger } from '../../helpers/logger' 54import { logger } from '../../helpers/logger'
55import { CONFIG } from '../../initializers/config'
46import { 56import {
47 ACTIVITY_PUB, 57 ACTIVITY_PUB,
48 API_VERSION, 58 API_VERSION,
@@ -59,40 +69,6 @@ import {
59 WEBSERVER 69 WEBSERVER
60} from '../../initializers/constants' 70} from '../../initializers/constants'
61import { sendDeleteVideo } from '../../lib/activitypub/send' 71import { sendDeleteVideo } from '../../lib/activitypub/send'
62import { AccountModel } from '../account/account'
63import { AccountVideoRateModel } from '../account/account-video-rate'
64import { ActorModel } from '../activitypub/actor'
65import { AvatarModel } from '../avatar/avatar'
66import { ServerModel } from '../server/server'
67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
68import { TagModel } from './tag'
69import { VideoAbuseModel } from './video-abuse'
70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
71import { VideoCommentModel } from './video-comment'
72import { VideoFileModel } from './video-file'
73import { VideoShareModel } from './video-share'
74import { VideoTagModel } from './video-tag'
75import { ScheduleVideoUpdateModel } from './schedule-video-update'
76import { VideoCaptionModel } from './video-caption'
77import { VideoBlacklistModel } from './video-blacklist'
78import { remove } from 'fs-extra'
79import { VideoViewModel } from './video-view'
80import { VideoRedundancyModel } from '../redundancy/video-redundancy'
81import {
82 videoFilesModelToFormattedJSON,
83 VideoFormattingJSONOptions,
84 videoModelToActivityPubObject,
85 videoModelToFormattedDetailsJSON,
86 videoModelToFormattedJSON
87} from './video-format-utils'
88import { UserVideoHistoryModel } from '../account/user-video-history'
89import { VideoImportModel } from './video-import'
90import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
91import { VideoPlaylistElementModel } from './video-playlist-element'
92import { CONFIG } from '../../initializers/config'
93import { ThumbnailModel } from './thumbnail'
94import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
95import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
96import { 72import {
97 MChannel, 73 MChannel,
98 MChannelAccountDefault, 74 MChannelAccountDefault,
@@ -118,15 +94,39 @@ import {
118 MVideoWithFile, 94 MVideoWithFile,
119 MVideoWithRights 95 MVideoWithRights
120} from '../../types/models' 96} from '../../types/models'
121import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
122import { MThumbnail } from '../../types/models/video/thumbnail' 97import { MThumbnail } from '../../types/models/video/thumbnail'
123import { VideoFile } from '@shared/models/videos/video-file.model' 98import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 99import { VideoAbuseModel } from '../abuse/video-abuse'
125import { ModelCache } from '@server/models/model-cache' 100import { AccountModel } from '../account/account'
101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { UserVideoHistoryModel } from '../account/user-video-history'
103import { ActorModel } from '../activitypub/actor'
104import { AvatarModel } from '../avatar/avatar'
105import { VideoRedundancyModel } from '../redundancy/video-redundancy'
106import { ServerModel } from '../server/server'
107import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
108import { ScheduleVideoUpdateModel } from './schedule-video-update'
109import { TagModel } from './tag'
110import { ThumbnailModel } from './thumbnail'
111import { VideoBlacklistModel } from './video-blacklist'
112import { VideoCaptionModel } from './video-caption'
113import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
114import { VideoCommentModel } from './video-comment'
115import { VideoFileModel } from './video-file'
116import {
117 videoFilesModelToFormattedJSON,
118 VideoFormattingJSONOptions,
119 videoModelToActivityPubObject,
120 videoModelToFormattedDetailsJSON,
121 videoModelToFormattedJSON
122} from './video-format-utils'
123import { VideoImportModel } from './video-import'
124import { VideoPlaylistElementModel } from './video-playlist-element'
126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' 125import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
127import { buildNSFWFilter } from '@server/helpers/express-utils' 126import { VideoShareModel } from './video-share'
128import { getServerActor } from '@server/models/application/application' 127import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
129import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" 128import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view'
130 130
131export enum ScopeNames { 131export enum ScopeNames {
132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> {
803 static async saveEssentialDataToAbuses (instance: VideoModel, options) { 803 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
804 const tasks: Promise<any>[] = [] 804 const tasks: Promise<any>[] = []
805 805
806 logger.info('Saving video abuses details of video %s.', instance.url)
807
808 if (!Array.isArray(instance.VideoAbuses)) { 806 if (!Array.isArray(instance.VideoAbuses)) {
809 instance.VideoAbuses = await instance.$get('VideoAbuses') 807 instance.VideoAbuses = await instance.$get('VideoAbuses')
810 808
811 if (instance.VideoAbuses.length === 0) return undefined 809 if (instance.VideoAbuses.length === 0) return undefined
812 } 810 }
813 811
812 logger.info('Saving video abuses details of video %s.', instance.url)
813
814 const details = instance.toFormattedDetailsJSON() 814 const details = instance.toFormattedDetailsJSON()
815 815
816 for (const abuse of instance.VideoAbuses) { 816 for (const abuse of instance.VideoAbuses) {
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts
new file mode 100644
index 000000000..8964c0ab2
--- /dev/null
+++ b/server/tests/api/check-params/abuses.ts
@@ -0,0 +1,269 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { AbuseCreate, AbuseState } from '@shared/models'
5import {
6 cleanupTests,
7 createUser,
8 deleteAbuse,
9 flushAndRunServer,
10 makeGetRequest,
11 makePostBodyRequest,
12 ServerInfo,
13 setAccessTokensToServers,
14 updateAbuse,
15 uploadVideo,
16 userLogin
17} from '../../../../shared/extra-utils'
18import {
19 checkBadCountPagination,
20 checkBadSortPagination,
21 checkBadStartPagination
22} from '../../../../shared/extra-utils/requests/check-api-params'
23
24describe('Test abuses API validators', function () {
25 const basePath = '/api/v1/abuses/'
26
27 let server: ServerInfo
28 let userAccessToken = ''
29 let abuseId: number
30
31 // ---------------------------------------------------------------
32
33 before(async function () {
34 this.timeout(30000)
35
36 server = await flushAndRunServer(1)
37
38 await setAccessTokensToServers([ server ])
39
40 const username = 'user1'
41 const password = 'my super password'
42 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
43 userAccessToken = await userLogin(server, { username, password })
44
45 const res = await uploadVideo(server.url, server.accessToken, {})
46 server.video = res.body.video
47 })
48
49 describe('When listing abuses', function () {
50 const path = basePath
51
52 it('Should fail with a bad start pagination', async function () {
53 await checkBadStartPagination(server.url, path, server.accessToken)
54 })
55
56 it('Should fail with a bad count pagination', async function () {
57 await checkBadCountPagination(server.url, path, server.accessToken)
58 })
59
60 it('Should fail with an incorrect sort', async function () {
61 await checkBadSortPagination(server.url, path, server.accessToken)
62 })
63
64 it('Should fail with a non authenticated user', async function () {
65 await makeGetRequest({
66 url: server.url,
67 path,
68 statusCodeExpected: 401
69 })
70 })
71
72 it('Should fail with a non admin user', async function () {
73 await makeGetRequest({
74 url: server.url,
75 path,
76 token: userAccessToken,
77 statusCodeExpected: 403
78 })
79 })
80
81 it('Should fail with a bad id filter', async function () {
82 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
83 })
84
85 it('Should fail with a bad filter', async function () {
86 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } })
87 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } })
88 })
89
90 it('Should fail with bad predefined reason', async function () {
91 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } })
92 })
93
94 it('Should fail with a bad state filter', async function () {
95 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
96 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } })
97 })
98
99 it('Should fail with a bad videoIs filter', async function () {
100 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
101 })
102
103 it('Should succeed with the correct params', async function () {
104 const query = {
105 id: 13,
106 predefinedReason: 'violentOrRepulsive',
107 filter: 'comment',
108 state: 2,
109 videoIs: 'deleted'
110 }
111
112 await makeGetRequest({ url: server.url, path, token: server.accessToken, query, statusCodeExpected: 200 })
113 })
114 })
115
116 describe('When reporting an abuse', function () {
117 const path = basePath
118
119 it('Should fail with nothing', async function () {
120 const fields = {}
121 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
122 })
123
124 it('Should fail with a wrong video', async function () {
125 const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
126 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
127 })
128
129 it('Should fail with an unknown video', async function () {
130 const fields = { video: { id: 42 }, reason: 'my super reason' }
131 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
132 })
133
134 it('Should fail with a wrong comment', async function () {
135 const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
136 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
137 })
138
139 it('Should fail with an unknown comment', async function () {
140 const fields = { comment: { id: 42 }, reason: 'my super reason' }
141 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
142 })
143
144 it('Should fail with a wrong account', async function () {
145 const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
146 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields })
147 })
148
149 it('Should fail with an unknown account', async function () {
150 const fields = { account: { id: 42 }, reason: 'my super reason' }
151 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 404 })
152 })
153
154 it('Should fail with not account, comment or video', async function () {
155 const fields = { reason: 'my super reason' }
156 await makePostBodyRequest({ url: server.url, path: path, token: server.accessToken, fields, statusCodeExpected: 400 })
157 })
158
159 it('Should fail with a non authenticated user', async function () {
160 const fields = { video: { id: server.video.id }, reason: 'my super reason' }
161
162 await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 })
163 })
164
165 it('Should fail with a reason too short', async function () {
166 const fields = { video: { id: server.video.id }, reason: 'h' }
167
168 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
169 })
170
171 it('Should fail with a too big reason', async function () {
172 const fields = { video: { id: server.video.id }, reason: 'super'.repeat(605) }
173
174 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
175 })
176
177 it('Should succeed with the correct parameters (basic)', async function () {
178 const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' }
179
180 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
181 abuseId = res.body.abuse.id
182 })
183
184 it('Should fail with a wrong predefined reason', async function () {
185 const fields = { video: { id: server.video.id }, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
186
187 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
188 })
189
190 it('Should fail with negative timestamps', async function () {
191 const fields = { video: { id: server.video.id, startAt: -1 }, reason: 'my super reason' }
192
193 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
194 })
195
196 it('Should fail mith misordered startAt/endAt', async function () {
197 const fields = { video: { id: server.video.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
198
199 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
200 })
201
202 it('Should succeed with the corret parameters (advanced)', async function () {
203 const fields: AbuseCreate = {
204 video: {
205 id: server.video.id,
206 startAt: 1,
207 endAt: 5
208 },
209 reason: 'my super reason',
210 predefinedReasons: [ 'serverRules' ]
211 }
212
213 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
214 })
215 })
216
217 describe('When updating an abuse', function () {
218
219 it('Should fail with a non authenticated user', async function () {
220 await updateAbuse(server.url, 'blabla', abuseId, {}, 401)
221 })
222
223 it('Should fail with a non admin user', async function () {
224 await updateAbuse(server.url, userAccessToken, abuseId, {}, 403)
225 })
226
227 it('Should fail with a bad abuse id', async function () {
228 await updateAbuse(server.url, server.accessToken, 45, {}, 404)
229 })
230
231 it('Should fail with a bad state', async function () {
232 const body = { state: 5 }
233 await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
234 })
235
236 it('Should fail with a bad moderation comment', async function () {
237 const body = { moderationComment: 'b'.repeat(3001) }
238 await updateAbuse(server.url, server.accessToken, abuseId, body, 400)
239 })
240
241 it('Should succeed with the correct params', async function () {
242 const body = { state: AbuseState.ACCEPTED }
243 await updateAbuse(server.url, server.accessToken, abuseId, body)
244 })
245 })
246
247 describe('When deleting a video abuse', function () {
248
249 it('Should fail with a non authenticated user', async function () {
250 await deleteAbuse(server.url, 'blabla', abuseId, 401)
251 })
252
253 it('Should fail with a non admin user', async function () {
254 await deleteAbuse(server.url, userAccessToken, abuseId, 403)
255 })
256
257 it('Should fail with a bad abuse id', async function () {
258 await deleteAbuse(server.url, server.accessToken, 45, 404)
259 })
260
261 it('Should succeed with the correct params', async function () {
262 await deleteAbuse(server.url, server.accessToken, abuseId)
263 })
264 })
265
266 after(async function () {
267 await cleanupTests([ server ])
268 })
269})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 93ffd98b1..0ee1f27aa 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,3 +1,4 @@
1import './abuses'
1import './accounts' 2import './accounts'
2import './blocklist' 3import './blocklist'
3import './bulk' 4import './bulk'
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 2048fa667..883b1d29c 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -164,7 +164,7 @@ describe('Test user notifications API validators', function () {
164 const correctFields: UserNotificationSetting = { 164 const correctFields: UserNotificationSetting = {
165 newVideoFromSubscription: UserNotificationSettingValue.WEB, 165 newVideoFromSubscription: UserNotificationSettingValue.WEB,
166 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 166 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
167 videoAbuseAsModerator: UserNotificationSettingValue.WEB, 167 abuseAsModerator: UserNotificationSettingValue.WEB,
168 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, 168 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
169 blacklistOnMyVideo: UserNotificationSettingValue.WEB, 169 blacklistOnMyVideo: UserNotificationSettingValue.WEB,
170 myVideoImportFinished: UserNotificationSettingValue.WEB, 170 myVideoImportFinished: UserNotificationSettingValue.WEB,
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index 557bf20eb..3b361ca79 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -1,7 +1,7 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4import { AbuseState, VideoAbuseCreate } from '@shared/models'
5import { 5import {
6 cleanupTests, 6 cleanupTests,
7 createUser, 7 createUser,
@@ -20,7 +20,8 @@ import {
20 checkBadSortPagination, 20 checkBadSortPagination,
21 checkBadStartPagination 21 checkBadStartPagination
22} from '../../../../shared/extra-utils/requests/check-api-params' 22} from '../../../../shared/extra-utils/requests/check-api-params'
23import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos' 23
24// FIXME: deprecated in 2.3. Remove this controller
24 25
25describe('Test video abuses API validators', function () { 26describe('Test video abuses API validators', function () {
26 let server: ServerInfo 27 let server: ServerInfo
@@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () {
136 const fields = { reason: 'my super reason' } 137 const fields = { reason: 'my super reason' }
137 138
138 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 }) 139 const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
139 videoAbuseId = res.body.videoAbuse.id 140 videoAbuseId = res.body.abuse.id
140 }) 141 })
141 142
142 it('Should fail with a wrong predefined reason', async function () { 143 it('Should fail with a wrong predefined reason', async function () {
@@ -151,12 +152,6 @@ describe('Test video abuses API validators', function () {
151 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 152 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
152 }) 153 })
153 154
154 it('Should fail mith misordered startAt/endAt', async function () {
155 const fields = { reason: 'my super reason', startAt: 5, endAt: 1 }
156
157 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
158 })
159
160 it('Should succeed with the corret parameters (advanced)', async function () { 155 it('Should succeed with the corret parameters (advanced)', async function () {
161 const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 } 156 const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 }
162 157
@@ -190,7 +185,7 @@ describe('Test video abuses API validators', function () {
190 }) 185 })
191 186
192 it('Should succeed with the correct params', async function () { 187 it('Should succeed with the correct params', async function () {
193 const body = { state: VideoAbuseState.ACCEPTED } 188 const body = { state: AbuseState.ACCEPTED }
194 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) 189 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
195 }) 190 })
196 }) 191 })
diff --git a/server/tests/api/ci-4.sh b/server/tests/api/ci-4.sh
index 14a014f07..4998de364 100644
--- a/server/tests/api/ci-4.sh
+++ b/server/tests/api/ci-4.sh
@@ -2,6 +2,7 @@
2 2
3set -eu 3set -eu
4 4
5activitypubFiles=$(find server/tests/api/moderation -type f | grep -v index.ts | xargs echo)
5redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo) 6redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo)
6activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo) 7activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo)
7 8
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index bac77ab2e..b62e2f5f7 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -1,6 +1,7 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './activitypub' 2import './activitypub'
3import './check-params' 3import './check-params'
4import './moderation'
4import './notifications' 5import './notifications'
5import './redundancy' 6import './redundancy'
6import './search' 7import './search'
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts
new file mode 100644
index 000000000..f186f7ea0
--- /dev/null
+++ b/server/tests/api/moderation/abuses.ts
@@ -0,0 +1,777 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { Abuse, AbuseFilter, AbusePredefinedReasonsString, AbuseState, VideoComment, Account } from '@shared/models'
6import {
7 addVideoCommentThread,
8 cleanupTests,
9 createUser,
10 deleteAbuse,
11 deleteVideoComment,
12 flushAndRunMultipleServers,
13 getAbusesList,
14 getVideoCommentThreads,
15 getVideoIdFromUUID,
16 getVideosList,
17 immutableAssign,
18 removeVideo,
19 reportAbuse,
20 ServerInfo,
21 setAccessTokensToServers,
22 updateAbuse,
23 uploadVideo,
24 uploadVideoAndGetId,
25 userLogin,
26 getAccount,
27 removeUser,
28 generateUserAccessToken
29} from '../../../../shared/extra-utils/index'
30import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
31import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
32import {
33 addAccountToServerBlocklist,
34 addServerToServerBlocklist,
35 removeAccountFromServerBlocklist,
36 removeServerFromServerBlocklist
37} from '../../../../shared/extra-utils/users/blocklist'
38
39const expect = chai.expect
40
41describe('Test abuses', function () {
42 let servers: ServerInfo[] = []
43 let abuseServer1: Abuse
44 let abuseServer2: Abuse
45
46 before(async function () {
47 this.timeout(50000)
48
49 // Run servers
50 servers = await flushAndRunMultipleServers(2)
51
52 // Get the access tokens
53 await setAccessTokensToServers(servers)
54
55 // Server 1 and server 2 follow each other
56 await doubleFollow(servers[0], servers[1])
57 })
58
59 describe('Video abuses', function () {
60
61 before(async function () {
62 this.timeout(50000)
63
64 // Upload some videos on each servers
65 const video1Attributes = {
66 name: 'my super name for server 1',
67 description: 'my super description for server 1'
68 }
69 await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes)
70
71 const video2Attributes = {
72 name: 'my super name for server 2',
73 description: 'my super description for server 2'
74 }
75 await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes)
76
77 // Wait videos propagation, server 2 has transcoding enabled
78 await waitJobs(servers)
79
80 const res = await getVideosList(servers[0].url)
81 const videos = res.body.data
82
83 expect(videos.length).to.equal(2)
84
85 servers[0].video = videos.find(video => video.name === 'my super name for server 1')
86 servers[1].video = videos.find(video => video.name === 'my super name for server 2')
87 })
88
89 it('Should not have abuses', async function () {
90 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
91
92 expect(res.body.total).to.equal(0)
93 expect(res.body.data).to.be.an('array')
94 expect(res.body.data.length).to.equal(0)
95 })
96
97 it('Should report abuse on a local video', async function () {
98 this.timeout(15000)
99
100 const reason = 'my super bad reason'
101 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[0].video.id, reason })
102
103 // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
104 await waitJobs(servers)
105 })
106
107 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
108 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
109
110 expect(res1.body.total).to.equal(1)
111 expect(res1.body.data).to.be.an('array')
112 expect(res1.body.data.length).to.equal(1)
113
114 const abuse: Abuse = res1.body.data[0]
115 expect(abuse.reason).to.equal('my super bad reason')
116
117 expect(abuse.reporterAccount.name).to.equal('root')
118 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
119
120 expect(abuse.video.id).to.equal(servers[0].video.id)
121 expect(abuse.video.channel).to.exist
122
123 expect(abuse.comment).to.be.null
124
125 expect(abuse.flaggedAccount.name).to.equal('root')
126 expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
127
128 expect(abuse.video.countReports).to.equal(1)
129 expect(abuse.video.nthReport).to.equal(1)
130
131 expect(abuse.countReportsForReporter).to.equal(1)
132 expect(abuse.countReportsForReportee).to.equal(1)
133
134 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
135 expect(res2.body.total).to.equal(0)
136 expect(res2.body.data).to.be.an('array')
137 expect(res2.body.data.length).to.equal(0)
138 })
139
140 it('Should report abuse on a remote video', async function () {
141 this.timeout(10000)
142
143 const reason = 'my super bad reason 2'
144 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[1].video.id, reason })
145
146 // We wait requests propagation
147 await waitJobs(servers)
148 })
149
150 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
151 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
152
153 expect(res1.body.total).to.equal(2)
154 expect(res1.body.data.length).to.equal(2)
155
156 const abuse1: Abuse = res1.body.data[0]
157 expect(abuse1.reason).to.equal('my super bad reason')
158 expect(abuse1.reporterAccount.name).to.equal('root')
159 expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
160
161 expect(abuse1.video.id).to.equal(servers[0].video.id)
162 expect(abuse1.video.countReports).to.equal(1)
163 expect(abuse1.video.nthReport).to.equal(1)
164
165 expect(abuse1.comment).to.be.null
166
167 expect(abuse1.flaggedAccount.name).to.equal('root')
168 expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
169
170 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
171 expect(abuse1.state.label).to.equal('Pending')
172 expect(abuse1.moderationComment).to.be.null
173
174 const abuse2: Abuse = res1.body.data[1]
175 expect(abuse2.reason).to.equal('my super bad reason 2')
176
177 expect(abuse2.reporterAccount.name).to.equal('root')
178 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
179
180 expect(abuse2.video.id).to.equal(servers[1].video.id)
181
182 expect(abuse2.comment).to.be.null
183
184 expect(abuse2.flaggedAccount.name).to.equal('root')
185 expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
186
187 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
188 expect(abuse2.state.label).to.equal('Pending')
189 expect(abuse2.moderationComment).to.be.null
190
191 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
192 expect(res2.body.total).to.equal(1)
193 expect(res2.body.data.length).to.equal(1)
194
195 abuseServer2 = res2.body.data[0]
196 expect(abuseServer2.reason).to.equal('my super bad reason 2')
197 expect(abuseServer2.reporterAccount.name).to.equal('root')
198 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
199
200 expect(abuse2.flaggedAccount.name).to.equal('root')
201 expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
202
203 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
204 expect(abuseServer2.state.label).to.equal('Pending')
205 expect(abuseServer2.moderationComment).to.be.null
206 })
207
208 it('Should hide video abuses from blocked accounts', async function () {
209 this.timeout(10000)
210
211 {
212 const videoId = await getVideoIdFromUUID(servers[1].url, servers[0].video.uuid)
213 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'will mute this' })
214 await waitJobs(servers)
215
216 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
217 expect(res.body.total).to.equal(3)
218 }
219
220 const accountToBlock = 'root@' + servers[1].host
221
222 {
223 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
224
225 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
226 expect(res.body.total).to.equal(2)
227
228 const abuse = res.body.data.find(a => a.reason === 'will mute this')
229 expect(abuse).to.be.undefined
230 }
231
232 {
233 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
234
235 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
236 expect(res.body.total).to.equal(3)
237 }
238 })
239
240 it('Should hide video abuses from blocked servers', async function () {
241 const serverToBlock = servers[1].host
242
243 {
244 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
245
246 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
247 expect(res.body.total).to.equal(2)
248
249 const abuse = res.body.data.find(a => a.reason === 'will mute this')
250 expect(abuse).to.be.undefined
251 }
252
253 {
254 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
255
256 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
257 expect(res.body.total).to.equal(3)
258 }
259 })
260
261 it('Should keep the video abuse when deleting the video', async function () {
262 this.timeout(10000)
263
264 await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid)
265
266 await waitJobs(servers)
267
268 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
269 expect(res.body.total).to.equal(2, "wrong number of videos returned")
270 expect(res.body.data).to.have.lengthOf(2, "wrong number of videos returned")
271
272 const abuse: Abuse = res.body.data[0]
273 expect(abuse.id).to.equal(abuseServer2.id, "wrong origin server id for first video")
274 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
275 expect(abuse.video.channel).to.exist
276 expect(abuse.video.deleted).to.be.true
277 })
278
279 it('Should include counts of reports from reporter and reportee', async function () {
280 this.timeout(10000)
281
282 // register a second user to have two reporters/reportees
283 const user = { username: 'user2', password: 'password' }
284 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user })
285 const userAccessToken = await userLogin(servers[0], user)
286
287 // upload a third video via this user
288 const video3Attributes = {
289 name: 'my second super name for server 1',
290 description: 'my second super description for server 1'
291 }
292 await uploadVideo(servers[0].url, userAccessToken, video3Attributes)
293
294 const res1 = await getVideosList(servers[0].url)
295 const videos = res1.body.data
296 const video3 = videos.find(video => video.name === 'my second super name for server 1')
297
298 // resume with the test
299 const reason3 = 'my super bad reason 3'
300 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video3.id, reason: reason3 })
301
302 const reason4 = 'my super bad reason 4'
303 await reportAbuse({ url: servers[0].url, token: userAccessToken, videoId: servers[0].video.id, reason: reason4 })
304
305 {
306 const res2 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
307 const abuses = res2.body.data as Abuse[]
308
309 const abuseVideo3 = res2.body.data.find(a => a.video.id === video3.id)
310 expect(abuseVideo3).to.not.be.undefined
311 expect(abuseVideo3.video.countReports).to.equal(1, "wrong reports count for video 3")
312 expect(abuseVideo3.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
313 expect(abuseVideo3.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
314 expect(abuseVideo3.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
315
316 const abuseServer1 = abuses.find(a => a.video.id === servers[0].video.id)
317 expect(abuseServer1.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse")
318 }
319 })
320
321 it('Should list predefined reasons as well as timestamps for the reported video', async function () {
322 this.timeout(10000)
323
324 const reason5 = 'my super bad reason 5'
325 const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
326 const createdAbuse = (await reportAbuse({
327 url: servers[0].url,
328 token: servers[0].accessToken,
329 videoId: servers[0].video.id,
330 reason: reason5,
331 predefinedReasons: predefinedReasons5,
332 startAt: 1,
333 endAt: 5
334 })).body.abuse
335
336 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
337
338 {
339 const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
340 expect(abuse.reason).to.equals(reason5)
341 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
342 expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
343 expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
344 }
345 })
346
347 it('Should delete the video abuse', async function () {
348 this.timeout(10000)
349
350 await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
351
352 await waitJobs(servers)
353
354 {
355 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
356 expect(res.body.total).to.equal(1)
357 expect(res.body.data.length).to.equal(1)
358 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
359 }
360
361 {
362 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
363 expect(res.body.total).to.equal(6)
364 }
365 })
366
367 it('Should list and filter video abuses', async function () {
368 this.timeout(10000)
369
370 async function list (query: Omit<Parameters<typeof getAbusesList>[0], 'url' | 'token'>) {
371 const options = {
372 url: servers[0].url,
373 token: servers[0].accessToken
374 }
375
376 Object.assign(options, query)
377
378 const res = await getAbusesList(options)
379
380 return res.body.data as Abuse[]
381 }
382
383 expect(await list({ id: 56 })).to.have.lengthOf(0)
384 expect(await list({ id: 1 })).to.have.lengthOf(1)
385
386 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
387 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
388
389 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
390
391 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
392 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
393
394 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
395 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
396
397 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
398 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
399
400 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
401 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
402
403 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
404 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
405
406 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
407 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
408 })
409 })
410
411 describe('Comment abuses', function () {
412
413 async function getComment (url: string, videoIdArg: number | string) {
414 const videoId = typeof videoIdArg === 'string'
415 ? await getVideoIdFromUUID(url, videoIdArg)
416 : videoIdArg
417
418 const res = await getVideoCommentThreads(url, videoId, 0, 5)
419
420 return res.body.data[0] as VideoComment
421 }
422
423 before(async function () {
424 this.timeout(50000)
425
426 servers[0].video = await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' })
427 servers[1].video = await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })
428
429 await addVideoCommentThread(servers[0].url, servers[0].accessToken, servers[0].video.id, 'comment server 1')
430 await addVideoCommentThread(servers[1].url, servers[1].accessToken, servers[1].video.id, 'comment server 2')
431
432 await waitJobs(servers)
433 })
434
435 it('Should report abuse on a comment', async function () {
436 this.timeout(15000)
437
438 const comment = await getComment(servers[0].url, servers[0].video.id)
439
440 const reason = 'it is a bad comment'
441 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason })
442
443 await waitJobs(servers)
444 })
445
446 it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () {
447 {
448 const comment = await getComment(servers[0].url, servers[0].video.id)
449 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
450
451 expect(res.body.total).to.equal(1)
452 expect(res.body.data).to.have.lengthOf(1)
453
454 const abuse: Abuse = res.body.data[0]
455 expect(abuse.reason).to.equal('it is a bad comment')
456
457 expect(abuse.reporterAccount.name).to.equal('root')
458 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
459
460 expect(abuse.video).to.be.null
461
462 expect(abuse.comment.deleted).to.be.false
463 expect(abuse.comment.id).to.equal(comment.id)
464 expect(abuse.comment.text).to.equal(comment.text)
465 expect(abuse.comment.video.name).to.equal('server 1')
466 expect(abuse.comment.video.id).to.equal(servers[0].video.id)
467 expect(abuse.comment.video.uuid).to.equal(servers[0].video.uuid)
468
469 expect(abuse.countReportsForReporter).to.equal(5)
470 expect(abuse.countReportsForReportee).to.equal(5)
471 }
472
473 {
474 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
475 expect(res.body.total).to.equal(0)
476 expect(res.body.data.length).to.equal(0)
477 }
478 })
479
480 it('Should report abuse on a remote comment', async function () {
481 this.timeout(10000)
482
483 const comment = await getComment(servers[0].url, servers[1].video.uuid)
484
485 const reason = 'it is a really bad comment'
486 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason })
487
488 await waitJobs(servers)
489 })
490
491 it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
492 const commentServer2 = await getComment(servers[0].url, servers[1].video.id)
493
494 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
495 expect(res1.body.total).to.equal(2)
496 expect(res1.body.data.length).to.equal(2)
497
498 const abuse: Abuse = res1.body.data[0]
499 expect(abuse.reason).to.equal('it is a bad comment')
500 expect(abuse.countReportsForReporter).to.equal(6)
501 expect(abuse.countReportsForReportee).to.equal(5)
502
503 const abuse2: Abuse = res1.body.data[1]
504
505 expect(abuse2.reason).to.equal('it is a really bad comment')
506
507 expect(abuse2.reporterAccount.name).to.equal('root')
508 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
509
510 expect(abuse2.video).to.be.null
511
512 expect(abuse2.comment.deleted).to.be.false
513 expect(abuse2.comment.id).to.equal(commentServer2.id)
514 expect(abuse2.comment.text).to.equal(commentServer2.text)
515 expect(abuse2.comment.video.name).to.equal('server 2')
516 expect(abuse2.comment.video.uuid).to.equal(servers[1].video.uuid)
517
518 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
519 expect(abuse2.state.label).to.equal('Pending')
520
521 expect(abuse2.moderationComment).to.be.null
522
523 expect(abuse2.countReportsForReporter).to.equal(6)
524 expect(abuse2.countReportsForReportee).to.equal(2)
525
526 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
527 expect(res2.body.total).to.equal(1)
528 expect(res2.body.data.length).to.equal(1)
529
530 abuseServer2 = res2.body.data[0]
531 expect(abuseServer2.reason).to.equal('it is a really bad comment')
532 expect(abuseServer2.reporterAccount.name).to.equal('root')
533 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
534
535 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
536 expect(abuseServer2.state.label).to.equal('Pending')
537
538 expect(abuseServer2.moderationComment).to.be.null
539
540 expect(abuseServer2.countReportsForReporter).to.equal(1)
541 expect(abuseServer2.countReportsForReportee).to.equal(1)
542 })
543
544 it('Should keep the comment abuse when deleting the comment', async function () {
545 this.timeout(10000)
546
547 const commentServer2 = await getComment(servers[0].url, servers[1].video.id)
548
549 await deleteVideoComment(servers[0].url, servers[0].accessToken, servers[1].video.uuid, commentServer2.id)
550
551 await waitJobs(servers)
552
553 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
554 expect(res.body.total).to.equal(2)
555 expect(res.body.data).to.have.lengthOf(2)
556
557 const abuse = (res.body.data as Abuse[]).find(a => a.comment?.id === commentServer2.id)
558 expect(abuse).to.not.be.undefined
559
560 expect(abuse.comment.text).to.be.empty
561 expect(abuse.comment.video.name).to.equal('server 2')
562 expect(abuse.comment.deleted).to.be.true
563 })
564
565 it('Should delete the comment abuse', async function () {
566 this.timeout(10000)
567
568 await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
569
570 await waitJobs(servers)
571
572 {
573 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
574 expect(res.body.total).to.equal(0)
575 expect(res.body.data.length).to.equal(0)
576 }
577
578 {
579 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment' })
580 expect(res.body.total).to.equal(2)
581 }
582 })
583
584 it('Should list and filter video abuses', async function () {
585 {
586 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'foo' })
587 expect(res.body.total).to.equal(0)
588 }
589
590 {
591 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'comment', searchReportee: 'ot' })
592 expect(res.body.total).to.equal(2)
593 }
594
595 {
596 const baseParams = { url: servers[0].url, token: servers[0].accessToken, filter: 'comment' as AbuseFilter, start: 1, count: 1 }
597
598 const res1 = await getAbusesList(immutableAssign(baseParams, { sort: 'createdAt' }))
599 expect(res1.body.data).to.have.lengthOf(1)
600 expect(res1.body.data[0].comment.text).to.be.empty
601
602 const res2 = await getAbusesList(immutableAssign(baseParams, { sort: '-createdAt' }))
603 expect(res2.body.data).to.have.lengthOf(1)
604 expect(res2.body.data[0].comment.text).to.equal('comment server 1')
605 }
606 })
607 })
608
609 describe('Account abuses', function () {
610
611 async function getAccountFromServer (url: string, name: string, server: ServerInfo) {
612 const res = await getAccount(url, name + '@' + server.host)
613
614 return res.body as Account
615 }
616
617 before(async function () {
618 this.timeout(50000)
619
620 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user_1', password: 'donald' })
621
622 const token = await generateUserAccessToken(servers[1], 'user_2')
623 await uploadVideo(servers[1].url, token, { name: 'super video' })
624
625 await waitJobs(servers)
626 })
627
628 it('Should report abuse on an account', async function () {
629 this.timeout(15000)
630
631 const account = await getAccountFromServer(servers[0].url, 'user_1', servers[0])
632
633 const reason = 'it is a bad account'
634 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason })
635
636 await waitJobs(servers)
637 })
638
639 it('Should have 1 account abuse on server 1 and 0 on server 2', async function () {
640 {
641 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
642
643 expect(res.body.total).to.equal(1)
644 expect(res.body.data).to.have.lengthOf(1)
645
646 const abuse: Abuse = res.body.data[0]
647 expect(abuse.reason).to.equal('it is a bad account')
648
649 expect(abuse.reporterAccount.name).to.equal('root')
650 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
651
652 expect(abuse.video).to.be.null
653 expect(abuse.comment).to.be.null
654
655 expect(abuse.flaggedAccount.name).to.equal('user_1')
656 expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
657 }
658
659 {
660 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'comment' })
661 expect(res.body.total).to.equal(0)
662 expect(res.body.data.length).to.equal(0)
663 }
664 })
665
666 it('Should report abuse on a remote account', async function () {
667 this.timeout(10000)
668
669 const account = await getAccountFromServer(servers[0].url, 'user_2', servers[1])
670
671 const reason = 'it is a really bad account'
672 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId: account.id, reason })
673
674 await waitJobs(servers)
675 })
676
677 it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
678 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
679 expect(res1.body.total).to.equal(2)
680 expect(res1.body.data.length).to.equal(2)
681
682 const abuse: Abuse = res1.body.data[0]
683 expect(abuse.reason).to.equal('it is a bad account')
684
685 const abuse2: Abuse = res1.body.data[1]
686 expect(abuse2.reason).to.equal('it is a really bad account')
687
688 expect(abuse2.reporterAccount.name).to.equal('root')
689 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
690
691 expect(abuse2.video).to.be.null
692 expect(abuse2.comment).to.be.null
693
694 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
695 expect(abuse2.state.label).to.equal('Pending')
696
697 expect(abuse2.moderationComment).to.be.null
698
699 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
700 expect(res2.body.total).to.equal(1)
701 expect(res2.body.data.length).to.equal(1)
702
703 abuseServer2 = res2.body.data[0]
704
705 expect(abuseServer2.reason).to.equal('it is a really bad account')
706
707 expect(abuseServer2.reporterAccount.name).to.equal('root')
708 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
709
710 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
711 expect(abuseServer2.state.label).to.equal('Pending')
712
713 expect(abuseServer2.moderationComment).to.be.null
714 })
715
716 it('Should keep the account abuse when deleting the account', async function () {
717 this.timeout(10000)
718
719 const account = await getAccountFromServer(servers[1].url, 'user_2', servers[1])
720 await removeUser(servers[1].url, account.userId, servers[1].accessToken)
721
722 await waitJobs(servers)
723
724 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
725 expect(res.body.total).to.equal(2)
726 expect(res.body.data).to.have.lengthOf(2)
727
728 const abuse = (res.body.data as Abuse[]).find(a => a.reason === 'it is a really bad account')
729 expect(abuse).to.not.be.undefined
730 })
731
732 it('Should delete the account abuse', async function () {
733 this.timeout(10000)
734
735 await deleteAbuse(servers[1].url, servers[1].accessToken, abuseServer2.id)
736
737 await waitJobs(servers)
738
739 {
740 const res = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken, filter: 'account' })
741 expect(res.body.total).to.equal(0)
742 expect(res.body.data.length).to.equal(0)
743 }
744
745 {
746 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, filter: 'account' })
747 expect(res.body.total).to.equal(2)
748
749 abuseServer1 = res.body.data[0]
750 }
751 })
752 })
753
754 describe('Common actions on abuses', function () {
755
756 it('Should update the state of an abuse', async function () {
757 const body = { state: AbuseState.REJECTED }
758 await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
759
760 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
761 expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
762 })
763
764 it('Should add a moderation comment', async function () {
765 const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
766 await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
767
768 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
769 expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
770 expect(res.body.data[0].moderationComment).to.equal('It is valid')
771 })
772 })
773
774 after(async function () {
775 await cleanupTests(servers)
776 })
777})
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/moderation/blocklist.ts
index 8c9107a50..8c9107a50 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/moderation/blocklist.ts
diff --git a/server/tests/api/moderation/index.ts b/server/tests/api/moderation/index.ts
new file mode 100644
index 000000000..cb018d88e
--- /dev/null
+++ b/server/tests/api/moderation/index.ts
@@ -0,0 +1,2 @@
1export * from './abuses'
2export * from './blocklist'
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts
index b90732a7a..a8517600a 100644
--- a/server/tests/api/notifications/moderation-notifications.ts
+++ b/server/tests/api/notifications/moderation-notifications.ts
@@ -3,15 +3,21 @@
3import 'mocha' 3import 'mocha'
4import { v4 as uuidv4 } from 'uuid' 4import { v4 as uuidv4 } from 'uuid'
5import { 5import {
6 addVideoCommentThread,
6 addVideoToBlacklist, 7 addVideoToBlacklist,
7 cleanupTests, 8 cleanupTests,
9 createUser,
8 follow, 10 follow,
11 generateUserAccessToken,
12 getAccount,
9 getCustomConfig, 13 getCustomConfig,
14 getVideoCommentThreads,
15 getVideoIdFromUUID,
10 immutableAssign, 16 immutableAssign,
11 MockInstancesIndex, 17 MockInstancesIndex,
12 registerUser, 18 registerUser,
13 removeVideoFromBlacklist, 19 removeVideoFromBlacklist,
14 reportVideoAbuse, 20 reportAbuse,
15 unfollow, 21 unfollow,
16 updateCustomConfig, 22 updateCustomConfig,
17 updateCustomSubConfig, 23 updateCustomSubConfig,
@@ -23,7 +29,9 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
23import { 29import {
24 checkAutoInstanceFollowing, 30 checkAutoInstanceFollowing,
25 CheckerBaseParams, 31 CheckerBaseParams,
32 checkNewAccountAbuseForModerators,
26 checkNewBlacklistOnMyVideo, 33 checkNewBlacklistOnMyVideo,
34 checkNewCommentAbuseForModerators,
27 checkNewInstanceFollower, 35 checkNewInstanceFollower,
28 checkNewVideoAbuseForModerators, 36 checkNewVideoAbuseForModerators,
29 checkNewVideoFromSubscription, 37 checkNewVideoFromSubscription,
@@ -74,12 +82,12 @@ describe('Test moderation notifications', function () {
74 82
75 const name = 'video for abuse ' + uuidv4() 83 const name = 'video for abuse ' + uuidv4()
76 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) 84 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
77 const uuid = resVideo.body.video.uuid 85 const video = resVideo.body.video
78 86
79 await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') 87 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: video.id, reason: 'super reason' })
80 88
81 await waitJobs(servers) 89 await waitJobs(servers)
82 await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') 90 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
83 }) 91 })
84 92
85 it('Should send a notification to moderators on remote video abuse', async function () { 93 it('Should send a notification to moderators on remote video abuse', async function () {
@@ -87,14 +95,77 @@ describe('Test moderation notifications', function () {
87 95
88 const name = 'video for abuse ' + uuidv4() 96 const name = 'video for abuse ' + uuidv4()
89 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) 97 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
90 const uuid = resVideo.body.video.uuid 98 const video = resVideo.body.video
99
100 await waitJobs(servers)
101
102 const videoId = await getVideoIdFromUUID(servers[1].url, video.uuid)
103 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId, reason: 'super reason' })
104
105 await waitJobs(servers)
106 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
107 })
108
109 it('Should send a notification to moderators on local comment abuse', async function () {
110 this.timeout(10000)
111
112 const name = 'video for abuse ' + uuidv4()
113 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
114 const video = resVideo.body.video
115 const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4())
116 const comment = resComment.body.comment
117
118 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, commentId: comment.id, reason: 'super reason' })
119
120 await waitJobs(servers)
121 await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence')
122 })
123
124 it('Should send a notification to moderators on remote comment abuse', async function () {
125 this.timeout(10000)
126
127 const name = 'video for abuse ' + uuidv4()
128 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name })
129 const video = resVideo.body.video
130 await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4())
131
132 await waitJobs(servers)
133
134 const resComments = await getVideoCommentThreads(servers[1].url, video.uuid, 0, 5)
135 const commentId = resComments.body.data[0].id
136 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, commentId, reason: 'super reason' })
137
138 await waitJobs(servers)
139 await checkNewCommentAbuseForModerators(baseParams, video.uuid, name, 'presence')
140 })
141
142 it('Should send a notification to moderators on local account abuse', async function () {
143 this.timeout(10000)
144
145 const username = 'user' + new Date().getTime()
146 const resUser = await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username, password: 'donald' })
147 const accountId = resUser.body.user.account.id
148
149 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, accountId, reason: 'super reason' })
150
151 await waitJobs(servers)
152 await checkNewAccountAbuseForModerators(baseParams, username, 'presence')
153 })
154
155 it('Should send a notification to moderators on remote account abuse', async function () {
156 this.timeout(10000)
157
158 const username = 'user' + new Date().getTime()
159 const tmpToken = await generateUserAccessToken(servers[0], username)
160 await uploadVideo(servers[0].url, tmpToken, { name: 'super video' })
91 161
92 await waitJobs(servers) 162 await waitJobs(servers)
93 163
94 await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') 164 const resAccount = await getAccount(servers[1].url, username + '@' + servers[0].host)
165 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, accountId: resAccount.body.id, reason: 'super reason' })
95 166
96 await waitJobs(servers) 167 await waitJobs(servers)
97 await checkNewVideoAbuseForModerators(baseParams, uuid, name, 'presence') 168 await checkNewAccountAbuseForModerators(baseParams, username, 'presence')
98 }) 169 })
99 }) 170 })
100 171
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 95b64a459..b01a91d48 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -1,7 +1,7 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { 5import {
6 addVideoToBlacklist, 6 addVideoToBlacklist,
7 askResetPassword, 7 askResetPassword,
@@ -11,7 +11,7 @@ import {
11 createUser, 11 createUser,
12 flushAndRunServer, 12 flushAndRunServer,
13 removeVideoFromBlacklist, 13 removeVideoFromBlacklist,
14 reportVideoAbuse, 14 reportAbuse,
15 resetPassword, 15 resetPassword,
16 ServerInfo, 16 ServerInfo,
17 setAccessTokensToServers, 17 setAccessTokensToServers,
@@ -30,10 +30,15 @@ describe('Test emails', function () {
30 let userId: number 30 let userId: number
31 let userId2: number 31 let userId2: number
32 let userAccessToken: string 32 let userAccessToken: string
33
33 let videoUUID: string 34 let videoUUID: string
35 let videoId: number
36
34 let videoUserUUID: string 37 let videoUserUUID: string
38
35 let verificationString: string 39 let verificationString: string
36 let verificationString2: string 40 let verificationString2: string
41
37 const emails: object[] = [] 42 const emails: object[] = []
38 const user = { 43 const user = {
39 username: 'user_1', 44 username: 'user_1',
@@ -76,6 +81,7 @@ describe('Test emails', function () {
76 } 81 }
77 const res = await uploadVideo(server.url, server.accessToken, attributes) 82 const res = await uploadVideo(server.url, server.accessToken, attributes)
78 videoUUID = res.body.video.uuid 83 videoUUID = res.body.video.uuid
84 videoId = res.body.video.id
79 } 85 }
80 }) 86 })
81 87
@@ -174,12 +180,12 @@ describe('Test emails', function () {
174 }) 180 })
175 }) 181 })
176 182
177 describe('When creating a video abuse', function () { 183 describe('When creating an abuse', function () {
178 it('Should send the notification email', async function () { 184 it('Should send the notification email', async function () {
179 this.timeout(10000) 185 this.timeout(10000)
180 186
181 const reason = 'my super bad reason' 187 const reason = 'my super bad reason'
182 await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) 188 await reportAbuse({ url: server.url, token: server.accessToken, videoId, reason })
183 189
184 await waitJobs(server) 190 await waitJobs(server)
185 expect(emails).to.have.lengthOf(3) 191 expect(emails).to.have.lengthOf(3)
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
index fcd022429..a244a6edb 100644
--- a/server/tests/api/users/index.ts
+++ b/server/tests/api/users/index.ts
@@ -1,5 +1,4 @@
1import './users-verification'
2import './blocklist'
3import './user-subscriptions' 1import './user-subscriptions'
4import './users' 2import './users'
5import './users-multiple-servers' 3import './users-multiple-servers'
4import './users-verification'
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 0a66bd1ce..ea74bde6a 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -1,8 +1,9 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' 4import * as chai from 'chai'
5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
6import { CustomConfig } from '@shared/models/server'
6import { 7import {
7 addVideoCommentThread, 8 addVideoCommentThread,
8 blockUser, 9 blockUser,
@@ -10,6 +11,7 @@ import {
10 createUser, 11 createUser,
11 deleteMe, 12 deleteMe,
12 flushAndRunServer, 13 flushAndRunServer,
14 getAbusesList,
13 getAccountRatings, 15 getAccountRatings,
14 getBlacklistedVideosList, 16 getBlacklistedVideosList,
15 getCustomConfig, 17 getCustomConfig,
@@ -19,7 +21,6 @@ import {
19 getUserInformation, 21 getUserInformation,
20 getUsersList, 22 getUsersList,
21 getUsersListPaginationAndSort, 23 getUsersListPaginationAndSort,
22 getVideoAbusesList,
23 getVideoChannel, 24 getVideoChannel,
24 getVideosList, 25 getVideosList,
25 installPlugin, 26 installPlugin,
@@ -29,15 +30,15 @@ import {
29 registerUserWithChannel, 30 registerUserWithChannel,
30 removeUser, 31 removeUser,
31 removeVideo, 32 removeVideo,
32 reportVideoAbuse, 33 reportAbuse,
33 ServerInfo, 34 ServerInfo,
34 testImage, 35 testImage,
35 unblockUser, 36 unblockUser,
37 updateAbuse,
36 updateCustomSubConfig, 38 updateCustomSubConfig,
37 updateMyAvatar, 39 updateMyAvatar,
38 updateMyUser, 40 updateMyUser,
39 updateUser, 41 updateUser,
40 updateVideoAbuse,
41 uploadVideo, 42 uploadVideo,
42 userLogin, 43 userLogin,
43 waitJobs 44 waitJobs
@@ -46,7 +47,6 @@ import { follow } from '../../../../shared/extra-utils/server/follows'
46import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 47import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
47import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 48import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
49import { CustomConfig } from '@shared/models/server'
50 50
51const expect = chai.expect 51const expect = chai.expect
52 52
@@ -302,10 +302,10 @@ describe('Test users', function () {
302 expect(userGet.videosCount).to.equal(0) 302 expect(userGet.videosCount).to.equal(0)
303 expect(userGet.videoCommentsCount).to.be.a('number') 303 expect(userGet.videoCommentsCount).to.be.a('number')
304 expect(userGet.videoCommentsCount).to.equal(0) 304 expect(userGet.videoCommentsCount).to.equal(0)
305 expect(userGet.videoAbusesCount).to.be.a('number') 305 expect(userGet.abusesCount).to.be.a('number')
306 expect(userGet.videoAbusesCount).to.equal(0) 306 expect(userGet.abusesCount).to.equal(0)
307 expect(userGet.videoAbusesAcceptedCount).to.be.a('number') 307 expect(userGet.abusesAcceptedCount).to.be.a('number')
308 expect(userGet.videoAbusesAcceptedCount).to.equal(0) 308 expect(userGet.abusesAcceptedCount).to.equal(0)
309 }) 309 })
310 }) 310 })
311 311
@@ -895,9 +895,9 @@ describe('Test users', function () {
895 895
896 expect(user.videosCount).to.equal(0) 896 expect(user.videosCount).to.equal(0)
897 expect(user.videoCommentsCount).to.equal(0) 897 expect(user.videoCommentsCount).to.equal(0)
898 expect(user.videoAbusesCount).to.equal(0) 898 expect(user.abusesCount).to.equal(0)
899 expect(user.videoAbusesCreatedCount).to.equal(0) 899 expect(user.abusesCreatedCount).to.equal(0)
900 expect(user.videoAbusesAcceptedCount).to.equal(0) 900 expect(user.abusesAcceptedCount).to.equal(0)
901 }) 901 })
902 902
903 it('Should report correct videos count', async function () { 903 it('Should report correct videos count', async function () {
@@ -924,26 +924,26 @@ describe('Test users', function () {
924 expect(user.videoCommentsCount).to.equal(1) 924 expect(user.videoCommentsCount).to.equal(1)
925 }) 925 })
926 926
927 it('Should report correct video abuses counts', async function () { 927 it('Should report correct abuses counts', async function () {
928 const reason = 'my super bad reason' 928 const reason = 'my super bad reason'
929 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason) 929 await reportAbuse({ url: server.url, token: user17AccessToken, videoId, reason })
930 930
931 const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken }) 931 const res1 = await getAbusesList({ url: server.url, token: server.accessToken })
932 const abuseId = res1.body.data[0].id 932 const abuseId = res1.body.data[0].id
933 933
934 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true) 934 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
935 const user2: User = res2.body 935 const user2: User = res2.body
936 936
937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations 937 expect(user2.abusesCount).to.equal(1) // number of incriminations
938 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created 938 expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
939 939
940 const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED } 940 const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
941 await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body) 941 await updateAbuse(server.url, server.accessToken, abuseId, body)
942 942
943 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true) 943 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
944 const user3: User = res3.body 944 const user3: User = res3.body
945 945
946 expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted 946 expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted
947 }) 947 })
948 }) 948 })
949 949
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 7383bd991..baeb543e0 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -1,21 +1,21 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos' 4import * as chai from 'chai'
5import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser,
8 deleteVideoAbuse, 9 deleteVideoAbuse,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
10 getVideoAbusesList, 11 getVideoAbusesList,
11 getVideosList, 12 getVideosList,
13 removeVideo,
12 reportVideoAbuse, 14 reportVideoAbuse,
13 ServerInfo, 15 ServerInfo,
14 setAccessTokensToServers, 16 setAccessTokensToServers,
15 updateVideoAbuse, 17 updateVideoAbuse,
16 uploadVideo, 18 uploadVideo,
17 removeVideo,
18 createUser,
19 userLogin 19 userLogin
20} from '../../../../shared/extra-utils/index' 20} from '../../../../shared/extra-utils/index'
21import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 21import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
@@ -29,9 +29,11 @@ import {
29 29
30const expect = chai.expect 30const expect = chai.expect
31 31
32// FIXME: deprecated in 2.3. Remove this controller
33
32describe('Test video abuses', function () { 34describe('Test video abuses', function () {
33 let servers: ServerInfo[] = [] 35 let servers: ServerInfo[] = []
34 let abuseServer2: VideoAbuse 36 let abuseServer2: Abuse
35 37
36 before(async function () { 38 before(async function () {
37 this.timeout(50000) 39 this.timeout(50000)
@@ -95,14 +97,14 @@ describe('Test video abuses', function () {
95 expect(res1.body.data).to.be.an('array') 97 expect(res1.body.data).to.be.an('array')
96 expect(res1.body.data.length).to.equal(1) 98 expect(res1.body.data.length).to.equal(1)
97 99
98 const abuse: VideoAbuse = res1.body.data[0] 100 const abuse: Abuse = res1.body.data[0]
99 expect(abuse.reason).to.equal('my super bad reason') 101 expect(abuse.reason).to.equal('my super bad reason')
100 expect(abuse.reporterAccount.name).to.equal('root') 102 expect(abuse.reporterAccount.name).to.equal('root')
101 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) 103 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
102 expect(abuse.video.id).to.equal(servers[0].video.id) 104 expect(abuse.video.id).to.equal(servers[0].video.id)
103 expect(abuse.video.channel).to.exist 105 expect(abuse.video.channel).to.exist
104 expect(abuse.count).to.equal(1) 106 expect(abuse.video.countReports).to.equal(1)
105 expect(abuse.nth).to.equal(1) 107 expect(abuse.video.nthReport).to.equal(1)
106 expect(abuse.countReportsForReporter).to.equal(1) 108 expect(abuse.countReportsForReporter).to.equal(1)
107 expect(abuse.countReportsForReportee).to.equal(1) 109 expect(abuse.countReportsForReportee).to.equal(1)
108 110
@@ -128,23 +130,23 @@ describe('Test video abuses', function () {
128 expect(res1.body.data).to.be.an('array') 130 expect(res1.body.data).to.be.an('array')
129 expect(res1.body.data.length).to.equal(2) 131 expect(res1.body.data.length).to.equal(2)
130 132
131 const abuse1: VideoAbuse = res1.body.data[0] 133 const abuse1: Abuse = res1.body.data[0]
132 expect(abuse1.reason).to.equal('my super bad reason') 134 expect(abuse1.reason).to.equal('my super bad reason')
133 expect(abuse1.reporterAccount.name).to.equal('root') 135 expect(abuse1.reporterAccount.name).to.equal('root')
134 expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port) 136 expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
135 expect(abuse1.video.id).to.equal(servers[0].video.id) 137 expect(abuse1.video.id).to.equal(servers[0].video.id)
136 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) 138 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
137 expect(abuse1.state.label).to.equal('Pending') 139 expect(abuse1.state.label).to.equal('Pending')
138 expect(abuse1.moderationComment).to.be.null 140 expect(abuse1.moderationComment).to.be.null
139 expect(abuse1.count).to.equal(1) 141 expect(abuse1.video.countReports).to.equal(1)
140 expect(abuse1.nth).to.equal(1) 142 expect(abuse1.video.nthReport).to.equal(1)
141 143
142 const abuse2: VideoAbuse = res1.body.data[1] 144 const abuse2: Abuse = res1.body.data[1]
143 expect(abuse2.reason).to.equal('my super bad reason 2') 145 expect(abuse2.reason).to.equal('my super bad reason 2')
144 expect(abuse2.reporterAccount.name).to.equal('root') 146 expect(abuse2.reporterAccount.name).to.equal('root')
145 expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port) 147 expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
146 expect(abuse2.video.id).to.equal(servers[1].video.id) 148 expect(abuse2.video.id).to.equal(servers[1].video.id)
147 expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING) 149 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
148 expect(abuse2.state.label).to.equal('Pending') 150 expect(abuse2.state.label).to.equal('Pending')
149 expect(abuse2.moderationComment).to.be.null 151 expect(abuse2.moderationComment).to.be.null
150 152
@@ -157,25 +159,25 @@ describe('Test video abuses', function () {
157 expect(abuseServer2.reason).to.equal('my super bad reason 2') 159 expect(abuseServer2.reason).to.equal('my super bad reason 2')
158 expect(abuseServer2.reporterAccount.name).to.equal('root') 160 expect(abuseServer2.reporterAccount.name).to.equal('root')
159 expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port) 161 expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
160 expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING) 162 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
161 expect(abuseServer2.state.label).to.equal('Pending') 163 expect(abuseServer2.state.label).to.equal('Pending')
162 expect(abuseServer2.moderationComment).to.be.null 164 expect(abuseServer2.moderationComment).to.be.null
163 }) 165 })
164 166
165 it('Should update the state of a video abuse', async function () { 167 it('Should update the state of a video abuse', async function () {
166 const body = { state: VideoAbuseState.REJECTED } 168 const body = { state: AbuseState.REJECTED }
167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 169 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
168 170
169 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) 171 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
170 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) 172 expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
171 }) 173 })
172 174
173 it('Should add a moderation comment', async function () { 175 it('Should add a moderation comment', async function () {
174 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } 176 const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 177 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
176 178
177 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) 179 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
178 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) 180 expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
179 expect(res.body.data[0].moderationComment).to.equal('It is valid') 181 expect(res.body.data[0].moderationComment).to.equal('It is valid')
180 }) 182 })
181 183
@@ -243,7 +245,7 @@ describe('Test video abuses', function () {
243 expect(res.body.data.length).to.equal(2, "wrong number of videos returned") 245 expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
244 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") 246 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
245 247
246 const abuse: VideoAbuse = res.body.data[0] 248 const abuse: Abuse = res.body.data[0]
247 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") 249 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
248 expect(abuse.video.channel).to.exist 250 expect(abuse.video.channel).to.exist
249 expect(abuse.video.deleted).to.be.true 251 expect(abuse.video.deleted).to.be.true
@@ -277,10 +279,10 @@ describe('Test video abuses', function () {
277 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 279 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
278 280
279 { 281 {
280 for (const abuse of res2.body.data as VideoAbuse[]) { 282 for (const abuse of res2.body.data as Abuse[]) {
281 if (abuse.video.id === video3.id) { 283 if (abuse.video.id === video3.id) {
282 expect(abuse.count).to.equal(1, "wrong reports count for video 3") 284 expect(abuse.video.countReports).to.equal(1, "wrong reports count for video 3")
283 expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") 285 expect(abuse.video.nthReport).to.equal(1, "wrong report position in report list for video 3")
284 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") 286 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
285 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") 287 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
286 } 288 }
@@ -295,7 +297,7 @@ describe('Test video abuses', function () {
295 this.timeout(10000) 297 this.timeout(10000)
296 298
297 const reason5 = 'my super bad reason 5' 299 const reason5 = 'my super bad reason 5'
298 const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] 300 const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
299 const createdAbuse = (await reportVideoAbuse( 301 const createdAbuse = (await reportVideoAbuse(
300 servers[0].url, 302 servers[0].url,
301 servers[0].accessToken, 303 servers[0].accessToken,
@@ -304,16 +306,16 @@ describe('Test video abuses', function () {
304 predefinedReasons5, 306 predefinedReasons5,
305 1, 307 1,
306 5 308 5
307 )).body.videoAbuse as VideoAbuse 309 )).body.abuse
308 310
309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 311 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
310 312
311 { 313 {
312 const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id) 314 const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
313 expect(abuse.reason).to.equals(reason5) 315 expect(abuse.reason).to.equals(reason5)
314 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") 316 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
315 expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported") 317 expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
316 expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported") 318 expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
317 } 319 }
318 }) 320 })
319 321
@@ -348,7 +350,7 @@ describe('Test video abuses', function () {
348 350
349 const res = await getVideoAbusesList(options) 351 const res = await getVideoAbusesList(options)
350 352
351 return res.body.data as VideoAbuse[] 353 return res.body.data as Abuse[]
352 } 354 }
353 355
354 expect(await list({ id: 56 })).to.have.lengthOf(0) 356 expect(await list({ id: 56 })).to.have.lengthOf(0)
@@ -365,14 +367,14 @@ describe('Test video abuses', function () {
365 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) 367 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
366 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5) 368 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
367 369
368 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4) 370 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
369 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) 371 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
370 372
371 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) 373 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
372 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) 374 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
373 375
374 expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0) 376 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
375 expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6) 377 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
376 378
377 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) 379 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
378 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) 380 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
diff --git a/server/types/models/index.ts b/server/types/models/index.ts
index 78b4948ce..affa17425 100644
--- a/server/types/models/index.ts
+++ b/server/types/models/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './moderation'
2export * from './oauth' 3export * from './oauth'
3export * from './server' 4export * from './server'
4export * from './user' 5export * from './user'
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
new file mode 100644
index 000000000..a0bf4b08f
--- /dev/null
+++ b/server/types/models/moderation/abuse.ts
@@ -0,0 +1,103 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { PickWith } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse'
5import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
6import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo, MComment, MCommentVideo } from '../video'
7import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
8
9type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
10type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
11type UseCommentAbuse<K extends keyof VideoCommentAbuseModel, M> = PickWith<VideoCommentAbuseModel, K, M>
12
13// ############################################################################
14
15export type MAbuse = Omit<AbuseModel, 'VideoCommentAbuse' | 'VideoAbuse' | 'ReporterAccount' | 'FlaggedAccount' | 'toActivityPubObject'>
16
17export type MVideoAbuse = Omit<VideoAbuseModel, 'Abuse' | 'Video'>
18
19export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'>
20
21// ############################################################################
22
23export type MVideoAbuseVideo =
24 MVideoAbuse &
25 UseVideoAbuse<'Video', MVideo>
26
27export type MVideoAbuseVideoUrl =
28 MVideoAbuse &
29 UseVideoAbuse<'Video', MVideoUrl>
30
31export type MVideoAbuseVideoFull =
32 MVideoAbuse &
33 UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles>
34
35export type MVideoAbuseFormattable =
36 MVideoAbuse &
37 UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
38 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
39
40// ############################################################################
41
42export type MCommentAbuseAccount =
43 MCommentAbuse &
44 UseCommentAbuse<'VideoComment', MCommentOwner>
45
46export type MCommentAbuseAccountVideo =
47 MCommentAbuse &
48 UseCommentAbuse<'VideoComment', MCommentOwnerVideo>
49
50export type MCommentAbuseUrl =
51 MCommentAbuse &
52 UseCommentAbuse<'VideoComment', MCommentUrl>
53
54export type MCommentAbuseFormattable =
55 MCommentAbuse &
56 UseCommentAbuse<'VideoComment', MComment & PickWith<MCommentVideo, 'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>>
57
58// ############################################################################
59
60export type MAbuseId = Pick<AbuseModel, 'id'>
61
62export type MAbuseVideo =
63 MAbuse &
64 Pick<AbuseModel, 'toActivityPubObject'> &
65 Use<'VideoAbuse', MVideoAbuseVideo>
66
67export type MAbuseUrl =
68 MAbuse &
69 Use<'VideoAbuse', MVideoAbuseVideoUrl> &
70 Use<'VideoCommentAbuse', MCommentAbuseUrl>
71
72export type MAbuseAccountVideo =
73 MAbuse &
74 Pick<AbuseModel, 'toActivityPubObject'> &
75 Use<'VideoAbuse', MVideoAbuseVideoFull> &
76 Use<'ReporterAccount', MAccountDefault>
77
78export type MAbuseAP =
79 MAbuse &
80 Pick<AbuseModel, 'toActivityPubObject'> &
81 Use<'ReporterAccount', MAccountUrl> &
82 Use<'FlaggedAccount', MAccountUrl> &
83 Use<'VideoAbuse', MVideoAbuseVideo> &
84 Use<'VideoCommentAbuse', MCommentAbuseAccount>
85
86export type MAbuseFull =
87 MAbuse &
88 Pick<AbuseModel, 'toActivityPubObject'> &
89 Use<'ReporterAccount', MAccountLight> &
90 Use<'FlaggedAccount', MAccountLight> &
91 Use<'VideoAbuse', MVideoAbuseVideoFull> &
92 Use<'VideoCommentAbuse', MCommentAbuseAccountVideo>
93
94// ############################################################################
95
96// Format for API or AP object
97
98export type MAbuseFormattable =
99 MAbuse &
100 Use<'ReporterAccount', MAccountFormattable> &
101 Use<'FlaggedAccount', MAccountFormattable> &
102 Use<'VideoAbuse', MVideoAbuseFormattable> &
103 Use<'VideoCommentAbuse', MCommentAbuseFormattable>
diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts
new file mode 100644
index 000000000..8bea1708f
--- /dev/null
+++ b/server/types/models/moderation/index.ts
@@ -0,0 +1 @@
export * from './abuse'
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index dd3de423b..f59eb7260 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -1,16 +1,18 @@
1import { UserNotificationModel } from '../../../models/account/user-notification' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 3import { PickWith, PickWithOpt } from '@shared/core-utils'
3import { VideoModel } from '../../../models/video/video' 4import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account'
6import { UserNotificationModel } from '../../../models/account/user-notification'
4import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
5import { ServerModel } from '../../../models/server/server' 8import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { AvatarModel } from '../../../models/avatar/avatar' 9import { AvatarModel } from '../../../models/avatar/avatar'
10import { ServerModel } from '../../../models/server/server'
11import { VideoModel } from '../../../models/video/video'
12import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
7import { VideoChannelModel } from '../../../models/video/video-channel' 13import { VideoChannelModel } from '../../../models/video/video-channel'
8import { AccountModel } from '../../../models/account/account'
9import { VideoCommentModel } from '../../../models/video/video-comment' 14import { VideoCommentModel } from '../../../models/video/video-comment'
10import { VideoAbuseModel } from '../../../models/video/video-abuse'
11import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
12import { VideoImportModel } from '../../../models/video/video-import' 15import { VideoImportModel } from '../../../models/video/video-import'
13import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
14 16
15type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M> 17type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
16 18
@@ -47,6 +49,18 @@ export module UserNotificationIncludes {
47 Pick<VideoAbuseModel, 'id'> & 49 Pick<VideoAbuseModel, 'id'> &
48 PickWith<VideoAbuseModel, 'Video', VideoInclude> 50 PickWith<VideoAbuseModel, 'Video', VideoInclude>
49 51
52 export type VideoCommentAbuseInclude =
53 Pick<VideoCommentAbuseModel, 'id'> &
54 PickWith<VideoCommentAbuseModel, 'VideoComment',
55 Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
56 PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>>
57
58 export type AbuseInclude =
59 Pick<AbuseModel, 'id'> &
60 PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> &
61 PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> &
62 PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor>
63
50 export type VideoBlacklistInclude = 64 export type VideoBlacklistInclude =
51 Pick<VideoBlacklistModel, 'id'> & 65 Pick<VideoBlacklistModel, 'id'> &
52 PickWith<VideoAbuseModel, 'Video', VideoInclude> 66 PickWith<VideoAbuseModel, 'Video', VideoInclude>
@@ -76,7 +90,7 @@ export module UserNotificationIncludes {
76// ############################################################################ 90// ############################################################################
77 91
78export type MUserNotification = 92export type MUserNotification =
79 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' | 93 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
80 'VideoImport' | 'Account' | 'ActorFollow'> 94 'VideoImport' | 'Account' | 'ActorFollow'>
81 95
82// ############################################################################ 96// ############################################################################
@@ -85,7 +99,7 @@ export type UserNotificationModelForApi =
85 MUserNotification & 99 MUserNotification &
86 Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & 100 Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
87 Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & 101 Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
88 Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> & 102 Use<'Abuse', UserNotificationIncludes.AbuseInclude> &
89 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & 103 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
90 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & 104 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
91 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & 105 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
diff --git a/server/types/models/video/index.ts b/server/types/models/video/index.ts
index bd69c8a4b..25db23898 100644
--- a/server/types/models/video/index.ts
+++ b/server/types/models/video/index.ts
@@ -2,7 +2,6 @@ export * from './schedule-video-update'
2export * from './tag' 2export * from './tag'
3export * from './thumbnail' 3export * from './thumbnail'
4export * from './video' 4export * from './video'
5export * from './video-abuse'
6export * from './video-blacklist' 5export * from './video-blacklist'
7export * from './video-caption' 6export * from './video-caption'
8export * from './video-change-ownership' 7export * from './video-change-ownership'
diff --git a/server/types/models/video/video-abuse.ts b/server/types/models/video/video-abuse.ts
deleted file mode 100644
index 279a87cf3..000000000
--- a/server/types/models/video/video-abuse.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import { VideoAbuseModel } from '../../../models/video/video-abuse'
2import { PickWith } from '@shared/core-utils'
3import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video'
4import { MAccountDefault, MAccountFormattable } from '../account'
5
6type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
7
8// ############################################################################
9
10export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'>
11
12// ############################################################################
13
14export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
15
16export type MVideoAbuseVideo =
17 MVideoAbuse &
18 Pick<VideoAbuseModel, 'toActivityPubObject'> &
19 Use<'Video', MVideo>
20
21export type MVideoAbuseAccountVideo =
22 MVideoAbuse &
23 Pick<VideoAbuseModel, 'toActivityPubObject'> &
24 Use<'Video', MVideoAccountLightBlacklistAllFiles> &
25 Use<'Account', MAccountDefault>
26
27// ############################################################################
28
29// Format for API or AP object
30
31export type MVideoAbuseFormattable =
32 MVideoAbuse &
33 Use<'Account', MAccountFormattable> &
34 Use<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
35 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index cac801e55..7595e6d86 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -1,5 +1,6 @@
1import { RegisterServerAuthExternalOptions } from '@server/types' 1import { RegisterServerAuthExternalOptions } from '@server/types'
2import { 2import {
3 MAbuse,
3 MAccountBlocklist, 4 MAccountBlocklist,
4 MActorUrl, 5 MActorUrl,
5 MStreamingPlaylist, 6 MStreamingPlaylist,
@@ -26,7 +27,6 @@ import {
26 MComment, 27 MComment,
27 MCommentOwnerVideoReply, 28 MCommentOwnerVideoReply,
28 MUserDefault, 29 MUserDefault,
29 MVideoAbuse,
30 MVideoBlacklist, 30 MVideoBlacklist,
31 MVideoCaptionVideo, 31 MVideoCaptionVideo,
32 MVideoFullLight, 32 MVideoFullLight,
@@ -77,7 +77,7 @@ declare module 'express' {
77 77
78 videoCaption?: MVideoCaptionVideo 78 videoCaption?: MVideoCaptionVideo
79 79
80 videoAbuse?: MVideoAbuse 80 abuse?: MAbuse
81 81
82 videoStreamingPlaylist?: MStreamingPlaylist 82 videoStreamingPlaylist?: MStreamingPlaylist
83 83