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/videos/abuse.ts109
-rw-r--r--server/helpers/audit-logger.ts27
-rw-r--r--server/helpers/custom-validators/abuses.ts54
-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/middlewares/abuses.ts (renamed from server/helpers/middlewares/video-abuses.ts)18
-rw-r--r--server/helpers/middlewares/index.ts2
-rw-r--r--server/initializers/constants.ts27
-rw-r--r--server/initializers/database.ts41
-rw-r--r--server/initializers/migrations/0250-video-abuse-state.ts4
-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.ts110
-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.pug15
-rw-r--r--server/lib/moderation.ts164
-rw-r--r--server/lib/notifier.ts43
-rw-r--r--server/middlewares/validators/abuse.ts253
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/sort.ts6
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts135
-rw-r--r--server/models/abuse/abuse.ts (renamed from server/models/video/video-abuse.ts)327
-rw-r--r--server/models/abuse/video-abuse.ts63
-rw-r--r--server/models/abuse/video-comment-abuse.ts53
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts4
-rw-r--r--server/models/account/user-notification.ts100
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/server/server-blocklist.ts10
-rw-r--r--server/models/video/video.ts84
-rw-r--r--server/tests/api/check-params/video-abuses.ts9
-rw-r--r--server/tests/api/users/users.ts4
-rw-r--r--server/tests/api/videos/video-abuse.ts54
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/moderation/abuse.ts97
-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
46 files changed, 1555 insertions, 764 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
new file mode 100644
index 000000000..ee046cb3a
--- /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('/abuse',
27 authenticate,
28 ensureUserHasRight(UserRight.MANAGE_ABUSES),
29 paginationValidator,
30 abusesSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 abuseListValidator,
34 asyncMiddleware(listAbuses)
35)
36abuseRouter.put('/:videoId/abuse/:id',
37 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_ABUSES),
39 asyncMiddleware(abuseUpdateValidator),
40 asyncRetryTransactionMiddleware(updateAbuse)
41)
42abuseRouter.post('/:videoId/abuse',
43 authenticate,
44 asyncMiddleware(abuseReportValidator),
45 asyncRetryTransactionMiddleware(reportAbuse)
46)
47abuseRouter.delete('/:videoId/abuse/: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: 'video',
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 video 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 video 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/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..a6a895c65
--- /dev/null
+++ b/server/helpers/custom-validators/abuses.ts
@@ -0,0 +1,54 @@
1import validator from 'validator'
2import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models'
3import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
4import { exists, isArray } from './misc'
5
6const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
7
8function isAbuseReasonValid (value: string) {
9 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
10}
11
12function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
13 return exists(value) && value in abusePredefinedReasonsMap
14}
15
16function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
17 return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
18}
19
20function isAbuseTimestampValid (value: number) {
21 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
22}
23
24function isAbuseTimestampCoherent (endAt: number, { req }) {
25 return exists(req.body.startAt) && endAt > req.body.startAt
26}
27
28function isAbuseModerationCommentValid (value: string) {
29 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
30}
31
32function isAbuseStateValid (value: string) {
33 return exists(value) && ABUSE_STATES[value] !== undefined
34}
35
36function isAbuseVideoIsValid (value: AbuseVideoIs) {
37 return exists(value) && (
38 value === 'deleted' ||
39 value === 'blacklisted'
40 )
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 isAbuseReasonValid,
47 isAbusePredefinedReasonValid,
48 isAbusePredefinedReasonsValid,
49 isAbuseTimestampValid,
50 isAbuseTimestampCoherent,
51 isAbuseModerationCommentValid,
52 isAbuseStateValid,
53 isAbuseVideoIsValid
54}
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/middlewares/video-abuses.ts b/server/helpers/middlewares/abuses.ts
index 97a5724b6..3906f6760 100644
--- a/server/helpers/middlewares/video-abuses.ts
+++ b/server/helpers/middlewares/abuses.ts
@@ -1,19 +1,20 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import { VideoAbuseModel } from '../../models/video/video-abuse' 2import { AbuseModel } from '../../models/abuse/abuse'
3import { fetchVideo } from '../video' 3import { fetchVideo } from '../video'
4 4
5// FIXME: deprecated in 2.3. Remove this function
5async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) { 6async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
6 const abuseId = parseInt(abuseIdArg + '', 10) 7 const abuseId = parseInt(abuseIdArg + '', 10)
7 let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID) 8 let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
8 9
9 if (!videoAbuse) { 10 if (!abuse) {
10 const userId = res.locals.oauth?.token.User.id 11 const userId = res.locals.oauth?.token.User.id
11 const video = await fetchVideo(videoUUID, 'all', userId) 12 const video = await fetchVideo(videoUUID, 'all', userId)
12 13
13 if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id) 14 if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id)
14 } 15 }
15 16
16 if (videoAbuse === null) { 17 if (abuse === null) {
17 res.status(404) 18 res.status(404)
18 .json({ error: 'Video abuse not found' }) 19 .json({ error: 'Video abuse not found' })
19 .end() 20 .end()
@@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
21 return false 22 return false
22 } 23 }
23 24
24 res.locals.videoAbuse = videoAbuse 25 res.locals.abuse = abuse
25 return true 26 return true
26} 27}
27 28
29async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
30
31}
32
28// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
29 34
30export { 35export {
36 doesAbuseExist,
31 doesVideoAbuseExist 37 doesVideoAbuseExist
32} 38}
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/initializers/constants.ts b/server/initializers/constants.ts
index e730e3c84..8f86bbbef 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'
@@ -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/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..e821aea5f 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,70 @@ 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: video.VideoChannel,
315 action
316 }
317 }
318 } else if (abuseInstance.VideoCommentAbuse) {
319 const comment = abuseInstance.VideoCommentAbuse.VideoComment
320 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
321
322 emailPayload = {
323 template: 'comment-abuse-new',
324 to,
325 subject: `New comment abuse report from ${reporter}`,
326 locals: {
327 commentUrl,
328 isLocal: comment.isOwned(),
329 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
330 reason: abuse.reason,
331 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
332 action
333 }
334 }
335 } else {
336 const account = abuseInstance.FlaggedAccount
337 const accountUrl = account.getClientUrl()
338
339 emailPayload = {
340 template: 'account-abuse-new',
341 to,
342 subject: `New account abuse report from ${reporter}`,
343 locals: {
344 accountUrl,
345 accountDisplayName: account.getDisplayName(),
346 isLocal: account.isOwned(),
347 reason: abuse.reason,
348 action
313 } 349 }
314 } 350 }
315 } 351 }
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..06be8025b
--- /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..170b79576
--- /dev/null
+++ b/server/lib/emails/video-comment-abuse-new/html.pug
@@ -0,0 +1,15 @@
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 '}comment "
10 a(href=commentUrl) of #{flaggedAccount}
11 | created on #{commentCreatedAt}
12
13 p The reporter, #{reporter}, cited the following reason(s):
14 blockquote #{reason}
15 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..40cff66d2 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, MAbuseVideo, 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,37 @@ 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 || abuseInstance.VideoCommentAbuse?.VideoComment?.url
363
364 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
366 365
367 function settingGetter (user: MUserWithNotificationSetting) { 366 function settingGetter (user: MUserWithNotificationSetting) {
368 return user.NotificationSetting.videoAbuseAsModerator 367 return user.NotificationSetting.videoAbuseAsModerator
369 } 368 }
370 369
371 async function notificationCreator (user: MUserWithNotificationSetting) { 370 async function notificationCreator (user: MUserWithNotificationSetting) {
372 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 371 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
373 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 372 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
374 userId: user.id, 373 userId: user.id,
375 videoAbuseId: parameters.videoAbuse.id 374 abuseId: abuse.id
376 }) 375 })
377 notification.VideoAbuse = parameters.videoAbuseInstance 376 notification.Abuse = abuseInstance
378 377
379 return notification 378 return notification
380 } 379 }
381 380
382 function emailSender (emails: string[]) { 381 function emailSender (emails: string[]) {
383 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) 382 return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
384 } 383 }
385 384
386 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 385 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
new file mode 100644
index 000000000..f098e2ff9
--- /dev/null
+++ b/server/middlewares/validators/abuse.ts
@@ -0,0 +1,253 @@
1import * as express from 'express'
2import { body, param, query } from 'express-validator'
3import {
4 isAbuseModerationCommentValid,
5 isAbusePredefinedReasonsValid,
6 isAbusePredefinedReasonValid,
7 isAbuseReasonValid,
8 isAbuseStateValid,
9 isAbuseTimestampCoherent,
10 isAbuseTimestampValid,
11 isAbuseVideoIsValid
12} from '@server/helpers/custom-validators/abuses'
13import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
14import { logger } from '@server/helpers/logger'
15import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
16import { areValidationErrors } from './utils'
17
18const abuseReportValidator = [
19 param('videoId')
20 .custom(isIdOrUUIDValid)
21 .not()
22 .isEmpty()
23 .withMessage('Should have a valid videoId'),
24 body('reason')
25 .custom(isAbuseReasonValid)
26 .withMessage('Should have a valid reason'),
27 body('predefinedReasons')
28 .optional()
29 .custom(isAbusePredefinedReasonsValid)
30 .withMessage('Should have a valid list of predefined reasons'),
31 body('startAt')
32 .optional()
33 .customSanitizer(toIntOrNull)
34 .custom(isAbuseTimestampValid)
35 .withMessage('Should have valid starting time value'),
36 body('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 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46 logger.debug('Checking abuseReport parameters', { parameters: req.body })
47
48 if (areValidationErrors(req, res)) return
49 if (!await doesVideoExist(req.params.videoId, res)) return
50
51 // TODO: check comment or video (exlusive)
52
53 return next()
54 }
55]
56
57const abuseGetValidator = [
58 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
59 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
60
61 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
62 logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
63
64 if (areValidationErrors(req, res)) return
65 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
66
67 return next()
68 }
69]
70
71const abuseUpdateValidator = [
72 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
73 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
74 body('state')
75 .optional()
76 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
77 body('moderationComment')
78 .optional()
79 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
80
81 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82 logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
83
84 if (areValidationErrors(req, res)) return
85 // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
86
87 return next()
88 }
89]
90
91const abuseListValidator = [
92 query('id')
93 .optional()
94 .custom(isIdValid).withMessage('Should have a valid id'),
95 query('predefinedReason')
96 .optional()
97 .custom(isAbusePredefinedReasonValid)
98 .withMessage('Should have a valid predefinedReason'),
99 query('search')
100 .optional()
101 .custom(exists).withMessage('Should have a valid search'),
102 query('state')
103 .optional()
104 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
105 query('videoIs')
106 .optional()
107 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
108 query('searchReporter')
109 .optional()
110 .custom(exists).withMessage('Should have a valid reporter search'),
111 query('searchReportee')
112 .optional()
113 .custom(exists).withMessage('Should have a valid reportee search'),
114 query('searchVideo')
115 .optional()
116 .custom(exists).withMessage('Should have a valid video search'),
117 query('searchVideoChannel')
118 .optional()
119 .custom(exists).withMessage('Should have a valid video channel search'),
120
121 (req: express.Request, res: express.Response, next: express.NextFunction) => {
122 logger.debug('Checking abuseListValidator parameters', { parameters: req.body })
123
124 if (areValidationErrors(req, res)) return
125
126 return next()
127 }
128]
129
130// FIXME: deprecated in 2.3. Remove these validators
131
132const videoAbuseReportValidator = [
133 param('videoId')
134 .custom(isIdOrUUIDValid)
135 .not()
136 .isEmpty()
137 .withMessage('Should have a valid videoId'),
138 body('reason')
139 .custom(isAbuseReasonValid)
140 .withMessage('Should have a valid reason'),
141 body('predefinedReasons')
142 .optional()
143 .custom(isAbusePredefinedReasonsValid)
144 .withMessage('Should have a valid list of predefined reasons'),
145 body('startAt')
146 .optional()
147 .customSanitizer(toIntOrNull)
148 .custom(isAbuseTimestampValid)
149 .withMessage('Should have valid starting time value'),
150 body('endAt')
151 .optional()
152 .customSanitizer(toIntOrNull)
153 .custom(isAbuseTimestampValid)
154 .withMessage('Should have valid ending time value')
155 .bail()
156 .custom(isAbuseTimestampCoherent)
157 .withMessage('Should have a startAt timestamp beginning before endAt'),
158
159 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
160 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
161
162 if (areValidationErrors(req, res)) return
163 if (!await doesVideoExist(req.params.videoId, res)) return
164
165 return next()
166 }
167]
168
169const videoAbuseGetValidator = [
170 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
171 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
172
173 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
174 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
175
176 if (areValidationErrors(req, res)) return
177 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
178
179 return next()
180 }
181]
182
183const videoAbuseUpdateValidator = [
184 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
185 param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
186 body('state')
187 .optional()
188 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
189 body('moderationComment')
190 .optional()
191 .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
192
193 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
194 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
195
196 if (areValidationErrors(req, res)) return
197 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
198
199 return next()
200 }
201]
202
203const videoAbuseListValidator = [
204 query('id')
205 .optional()
206 .custom(isIdValid).withMessage('Should have a valid id'),
207 query('predefinedReason')
208 .optional()
209 .custom(isAbusePredefinedReasonValid)
210 .withMessage('Should have a valid predefinedReason'),
211 query('search')
212 .optional()
213 .custom(exists).withMessage('Should have a valid search'),
214 query('state')
215 .optional()
216 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
217 query('videoIs')
218 .optional()
219 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
220 query('searchReporter')
221 .optional()
222 .custom(exists).withMessage('Should have a valid reporter search'),
223 query('searchReportee')
224 .optional()
225 .custom(exists).withMessage('Should have a valid reportee search'),
226 query('searchVideo')
227 .optional()
228 .custom(exists).withMessage('Should have a valid video search'),
229 query('searchVideoChannel')
230 .optional()
231 .custom(exists).withMessage('Should have a valid video channel search'),
232
233 (req: express.Request, res: express.Response, next: express.NextFunction) => {
234 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
235
236 if (areValidationErrors(req, res)) return
237
238 return next()
239 }
240]
241
242// ---------------------------------------------------------------------------
243
244export {
245 abuseListValidator,
246 abuseReportValidator,
247 abuseGetValidator,
248 abuseUpdateValidator,
249 videoAbuseReportValidator,
250 videoAbuseGetValidator,
251 videoAbuseUpdateValidator,
252 videoAbuseListValidator
253}
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/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/models/video/video-abuse.ts b/server/models/abuse/abuse.ts
index 1319332f0..4f99f9c9b 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -1,5 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize' 2import { invert } from 'lodash'
3import { literal, Op, WhereOptions } from 'sequelize'
3import { 4import {
4 AllowNull, 5 AllowNull,
5 BelongsTo, 6 BelongsTo,
@@ -8,36 +9,35 @@ import {
8 DataType, 9 DataType,
9 Default, 10 Default,
10 ForeignKey, 11 ForeignKey,
12 HasOne,
11 Is, 13 Is,
12 Model, 14 Model,
13 Scopes, 15 Scopes,
14 Table, 16 Table,
15 UpdatedAt 17 UpdatedAt
16} from 'sequelize-typescript' 18} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' 19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
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 { 20import {
28 isVideoAbuseModerationCommentValid, 21 Abuse,
29 isVideoAbuseReasonValid, 22 AbuseObject,
30 isVideoAbuseStateValid 23 AbusePredefinedReasons,
31} from '../../helpers/custom-validators/video-abuses' 24 abusePredefinedReasonsMap,
32import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 25 AbusePredefinedReasonsString,
33import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' 26 AbuseState,
34import { AccountModel } from '../account/account' 27 AbuseVideoIs,
28 VideoAbuse
29} from '@shared/models'
30import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
31import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
32import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
33import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' 34import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from './thumbnail' 35import { ThumbnailModel } from '../video/thumbnail'
37import { VideoModel } from './video' 36import { VideoModel } from '../video/video'
38import { VideoBlacklistModel } from './video-blacklist' 37import { VideoBlacklistModel } from '../video/video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 38import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
40import { invert } from 'lodash' 39import { VideoAbuseModel } from './video-abuse'
40import { VideoCommentAbuseModel } from './video-comment-abuse'
41 41
42export enum ScopeNames { 42export enum ScopeNames {
43 FOR_API = 'FOR_API' 43 FOR_API = 'FOR_API'
@@ -49,20 +49,26 @@ export enum ScopeNames {
49 search?: string 49 search?: string
50 searchReporter?: string 50 searchReporter?: string
51 searchReportee?: string 51 searchReportee?: string
52
53 // video releated
52 searchVideo?: string 54 searchVideo?: string
53 searchVideoChannel?: string 55 searchVideoChannel?: string
56 videoIs?: AbuseVideoIs
54 57
55 // filters 58 // filters
56 id?: number 59 id?: number
57 predefinedReasonId?: number 60 predefinedReasonId?: number
61 filter?: AbuseFilter
58 62
59 state?: VideoAbuseState 63 state?: AbuseState
60 videoIs?: VideoAbuseVideoIs
61 64
62 // accountIds 65 // accountIds
63 serverAccountId: number 66 serverAccountId: number
64 userAccountId: number 67 userAccountId: number
65 }) => { 68 }) => {
69 const onlyBlacklisted = options.videoIs === 'blacklisted'
70 const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
71
66 const where = { 72 const where = {
67 reporterAccountId: { 73 reporterAccountId: {
68 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') 74 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
@@ -70,33 +76,36 @@ export enum ScopeNames {
70 } 76 }
71 77
72 if (options.search) { 78 if (options.search) {
79 const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
80
73 Object.assign(where, { 81 Object.assign(where, {
74 [Op.or]: [ 82 [Op.or]: [
75 { 83 {
76 [Op.and]: [ 84 [Op.and]: [
77 { videoId: { [Op.not]: null } }, 85 { '$VideoAbuse.videoId$': { [Op.not]: null } },
78 searchAttribute(options.search, '$Video.name$') 86 searchAttribute(options.search, '$VideoAbuse.Video.name$')
79 ] 87 ]
80 }, 88 },
81 { 89 {
82 [Op.and]: [ 90 [Op.and]: [
83 { videoId: { [Op.not]: null } }, 91 { '$VideoAbuse.videoId$': { [Op.not]: null } },
84 searchAttribute(options.search, '$Video.VideoChannel.name$') 92 searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
85 ] 93 ]
86 }, 94 },
87 { 95 {
88 [Op.and]: [ 96 [Op.and]: [
89 { deletedVideo: { [Op.not]: null } }, 97 { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
90 { deletedVideo: searchAttribute(options.search, 'name') } 98 literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
91 ] 99 ]
92 }, 100 },
93 { 101 {
94 [Op.and]: [ 102 [Op.and]: [
95 { deletedVideo: { [Op.not]: null } }, 103 { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
96 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } 104 literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`)
97 ] 105 ]
98 }, 106 },
99 searchAttribute(options.search, '$Account.name$') 107 searchAttribute(options.search, '$ReporterAccount.name$'),
108 searchAttribute(options.search, '$FlaggedAccount.name$')
100 ] 109 ]
101 }) 110 })
102 } 111 }
@@ -106,7 +115,7 @@ export enum ScopeNames {
106 115
107 if (options.videoIs === 'deleted') { 116 if (options.videoIs === 'deleted') {
108 Object.assign(where, { 117 Object.assign(where, {
109 deletedVideo: { 118 '$VideoAbuse.deletedVideo$': {
110 [Op.not]: null 119 [Op.not]: null
111 } 120 }
112 }) 121 })
@@ -120,8 +129,6 @@ export enum ScopeNames {
120 }) 129 })
121 } 130 }
122 131
123 const onlyBlacklisted = options.videoIs === 'blacklisted'
124
125 return { 132 return {
126 attributes: { 133 attributes: {
127 include: [ 134 include: [
@@ -131,7 +138,7 @@ export enum ScopeNames {
131 '(' + 138 '(' +
132 'SELECT count(*) ' + 139 'SELECT count(*) ' +
133 'FROM "videoAbuse" ' + 140 'FROM "videoAbuse" ' +
134 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + 141 'WHERE "videoId" = "VideoAbuse"."videoId" ' +
135 ')' 142 ')'
136 ), 143 ),
137 'countReportsForVideo' 144 'countReportsForVideo'
@@ -146,7 +153,7 @@ export enum ScopeNames {
146 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + 153 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
147 'FROM "videoAbuse" ' + 154 'FROM "videoAbuse" ' +
148 ') t ' + 155 ') t ' +
149 'WHERE t.id = "VideoAbuseModel".id ' + 156 'WHERE t.id = "VideoAbuse".id' +
150 ')' 157 ')'
151 ), 158 ),
152 'nthReportForVideo' 159 'nthReportForVideo'
@@ -159,7 +166,7 @@ export enum ScopeNames {
159 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 166 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
160 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 167 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
161 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + 168 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
162 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + 169 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
163 ')' 170 ')'
164 ), 171 ),
165 'countReportsForReporter__video' 172 'countReportsForReporter__video'
@@ -169,7 +176,7 @@ export enum ScopeNames {
169 '(' + 176 '(' +
170 'SELECT count(DISTINCT "videoAbuse"."id") ' + 177 'SELECT count(DISTINCT "videoAbuse"."id") ' +
171 'FROM "videoAbuse" ' + 178 'FROM "videoAbuse" ' +
172 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + 179 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
173 ')' 180 ')'
174 ), 181 ),
175 'countReportsForReporter__deletedVideo' 182 'countReportsForReporter__deletedVideo'
@@ -182,8 +189,8 @@ export enum ScopeNames {
182 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + 189 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
183 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 190 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
184 'INNER JOIN "account" ON ' + 191 'INNER JOIN "account" ON ' +
185 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + 192 '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
186 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + 193 `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
187 ')' 194 ')'
188 ), 195 ),
189 'countReportsForReportee__video' 196 'countReportsForReportee__video'
@@ -193,9 +200,9 @@ export enum ScopeNames {
193 '(' + 200 '(' +
194 'SELECT count(DISTINCT "videoAbuse"."id") ' + 201 'SELECT count(DISTINCT "videoAbuse"."id") ' +
195 'FROM "videoAbuse" ' + 202 'FROM "videoAbuse" ' +
196 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + 203 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
197 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + 204 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
198 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + 205 `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
199 ')' 206 ')'
200 ), 207 ),
201 'countReportsForReportee__deletedVideo' 208 'countReportsForReportee__deletedVideo'
@@ -204,32 +211,47 @@ export enum ScopeNames {
204 }, 211 },
205 include: [ 212 include: [
206 { 213 {
207 model: AccountModel, 214 model: AccountModel.scope(AccountScopeNames.SUMMARY),
215 as: 'ReporterAccount',
208 required: true, 216 required: true,
209 where: searchAttribute(options.searchReporter, 'name') 217 where: searchAttribute(options.searchReporter, 'name')
210 }, 218 },
211 { 219 {
212 model: VideoModel, 220 model: AccountModel.scope(AccountScopeNames.SUMMARY),
213 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), 221 as: 'FlaggedAccount',
214 where: searchAttribute(options.searchVideo, 'name'), 222 required: true,
223 where: searchAttribute(options.searchReportee, 'name')
224 },
225 {
226 model: VideoAbuseModel,
227 required: options.filter === 'video' || !!options.videoIs || videoRequired,
215 include: [ 228 include: [
216 { 229 {
217 model: ThumbnailModel 230 model: VideoModel,
218 }, 231 required: videoRequired,
219 { 232 where: searchAttribute(options.searchVideo, 'name'),
220 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
221 where: searchAttribute(options.searchVideoChannel, 'name'),
222 include: [ 233 include: [
223 { 234 {
224 model: AccountModel, 235 model: ThumbnailModel
225 where: searchAttribute(options.searchReportee, 'name') 236 },
237 {
238 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }),
239 where: searchAttribute(options.searchVideoChannel, 'name'),
240 required: true,
241 include: [
242 {
243 model: AccountModel.scope(AccountScopeNames.SUMMARY),
244 required: true,
245 where: searchAttribute(options.searchReportee, 'name')
246 }
247 ]
248 },
249 {
250 attributes: [ 'id', 'reason', 'unfederated' ],
251 model: VideoBlacklistModel,
252 required: onlyBlacklisted
226 } 253 }
227 ] 254 ]
228 },
229 {
230 attributes: [ 'id', 'reason', 'unfederated' ],
231 model: VideoBlacklistModel,
232 required: onlyBlacklisted
233 } 255 }
234 ] 256 ]
235 } 257 }
@@ -239,55 +261,40 @@ export enum ScopeNames {
239 } 261 }
240})) 262}))
241@Table({ 263@Table({
242 tableName: 'videoAbuse', 264 tableName: 'abuse',
243 indexes: [ 265 indexes: [
244 { 266 {
245 fields: [ 'videoId' ] 267 fields: [ 'reporterAccountId' ]
246 }, 268 },
247 { 269 {
248 fields: [ 'reporterAccountId' ] 270 fields: [ 'flaggedAccountId' ]
249 } 271 }
250 ] 272 ]
251}) 273})
252export class VideoAbuseModel extends Model<VideoAbuseModel> { 274export class AbuseModel extends Model<AbuseModel> {
253 275
254 @AllowNull(false) 276 @AllowNull(false)
255 @Default(null) 277 @Default(null)
256 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) 278 @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
257 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) 279 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
258 reason: string 280 reason: string
259 281
260 @AllowNull(false) 282 @AllowNull(false)
261 @Default(null) 283 @Default(null)
262 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) 284 @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
263 @Column 285 @Column
264 state: VideoAbuseState 286 state: AbuseState
265 287
266 @AllowNull(true) 288 @AllowNull(true)
267 @Default(null) 289 @Default(null)
268 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true)) 290 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
269 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) 291 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
270 moderationComment: string 292 moderationComment: string
271 293
272 @AllowNull(true) 294 @AllowNull(true)
273 @Default(null) 295 @Default(null)
274 @Column(DataType.JSONB)
275 deletedVideo: VideoDetails
276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER)) 296 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[] 297 predefinedReasons: AbusePredefinedReasons[]
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 298
292 @CreatedAt 299 @CreatedAt
293 createdAt: Date 300 createdAt: Date
@@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
301 308
302 @BelongsTo(() => AccountModel, { 309 @BelongsTo(() => AccountModel, {
303 foreignKey: { 310 foreignKey: {
311 name: 'reporterAccountId',
304 allowNull: true 312 allowNull: true
305 }, 313 },
314 as: 'ReporterAccount',
306 onDelete: 'set null' 315 onDelete: 'set null'
307 }) 316 })
308 Account: AccountModel 317 ReporterAccount: AccountModel
309 318
310 @ForeignKey(() => VideoModel) 319 @ForeignKey(() => AccountModel)
311 @Column 320 @Column
312 videoId: number 321 flaggedAccountId: number
313 322
314 @BelongsTo(() => VideoModel, { 323 @BelongsTo(() => AccountModel, {
315 foreignKey: { 324 foreignKey: {
325 name: 'flaggedAccountId',
316 allowNull: true 326 allowNull: true
317 }, 327 },
328 as: 'FlaggedAccount',
318 onDelete: 'set null' 329 onDelete: 'set null'
319 }) 330 })
320 Video: VideoModel 331 FlaggedAccount: AccountModel
332
333 @HasOne(() => VideoCommentAbuseModel, {
334 foreignKey: {
335 name: 'abuseId',
336 allowNull: false
337 },
338 onDelete: 'cascade'
339 })
340 VideoCommentAbuse: VideoCommentAbuseModel
321 341
322 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> { 342 @HasOne(() => VideoAbuseModel, {
323 const videoAttributes = {} 343 foreignKey: {
324 if (videoId) videoAttributes['videoId'] = videoId 344 name: 'abuseId',
325 if (uuid) videoAttributes['deletedVideo'] = { uuid } 345 allowNull: false
346 },
347 onDelete: 'cascade'
348 })
349 VideoAbuse: VideoAbuseModel
350
351 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
352 const videoWhere: WhereOptions = {}
353
354 if (videoId) videoWhere.videoId = videoId
355 if (uuid) videoWhere.deletedVideo = { uuid }
326 356
327 const query = { 357 const query = {
358 include: [
359 {
360 model: VideoAbuseModel,
361 required: true,
362 where: videoWhere
363 }
364 ],
328 where: { 365 where: {
329 id, 366 id
330 ...videoAttributes
331 } 367 }
332 } 368 }
333 return VideoAbuseModel.findOne(query) 369 return AbuseModel.findOne(query)
334 } 370 }
335 371
336 static listForApi (parameters: { 372 static listForApi (parameters: {
@@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
338 count: number 374 count: number
339 sort: string 375 sort: string
340 376
377 filter?: AbuseFilter
378
341 serverAccountId: number 379 serverAccountId: number
342 user?: MUserAccountId 380 user?: MUserAccountId
343 381
344 id?: number 382 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString 383 predefinedReason?: AbusePredefinedReasonsString
346 state?: VideoAbuseState 384 state?: AbuseState
347 videoIs?: VideoAbuseVideoIs 385 videoIs?: AbuseVideoIs
348 386
349 search?: string 387 search?: string
350 searchReporter?: string 388 searchReporter?: string
@@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
364 predefinedReason, 402 predefinedReason,
365 searchReportee, 403 searchReportee,
366 searchVideo, 404 searchVideo,
405 filter,
367 searchVideoChannel, 406 searchVideoChannel,
368 searchReporter, 407 searchReporter,
369 id 408 id
370 } = parameters 409 } = parameters
371 410
372 const userAccountId = user ? user.Account.id : undefined 411 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined 412 const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
374 413
375 const query = { 414 const query = {
376 offset: start, 415 offset: start,
377 limit: count, 416 limit: count,
378 order: getSort(sort), 417 order: getSort(sort),
379 col: 'VideoAbuseModel.id', 418 col: 'AbuseModel.id',
380 distinct: true 419 distinct: true
381 } 420 }
382 421
383 const filters = { 422 const filters = {
384 id, 423 id,
424 filter,
385 predefinedReasonId, 425 predefinedReasonId,
386 search, 426 search,
387 state, 427 state,
@@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
394 userAccountId 434 userAccountId
395 } 435 }
396 436
397 return VideoAbuseModel 437 return AbuseModel
398 .scope([ 438 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] } 439 { method: [ ScopeNames.FOR_API, filters ] }
400 ]) 440 ])
@@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
404 }) 444 })
405 } 445 }
406 446
407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { 447 toFormattedJSON (this: MAbuseFormattable): Abuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) 448 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
409 const countReportsForVideo = this.get('countReportsForVideo') as number 449 const countReportsForVideo = this.get('countReportsForVideo') as number
410 const nthReportForVideo = this.get('nthReportForVideo') as number 450 const nthReportForVideo = this.get('nthReportForVideo') as number
411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number 451 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
@@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
413 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number 453 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
414 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number 454 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
415 455
416 const video = this.Video 456 let video: VideoAbuse
417 ? this.Video 457
418 : this.deletedVideo 458 if (this.VideoAbuse) {
459 const abuseModel = this.VideoAbuse
460 const entity = abuseModel.Video || abuseModel.deletedVideo
461
462 video = {
463 id: entity.id,
464 uuid: entity.uuid,
465 name: entity.name,
466 nsfw: entity.nsfw,
467
468 startAt: abuseModel.startAt,
469 endAt: abuseModel.endAt,
470
471 deleted: !abuseModel.Video,
472 blacklisted: abuseModel.Video?.isBlacklisted() || false,
473 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
474 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
475 }
476 }
419 477
420 return { 478 return {
421 id: this.id, 479 id: this.id,
422 reason: this.reason, 480 reason: this.reason,
423 predefinedReasons, 481 predefinedReasons,
424 reporterAccount: this.Account.toFormattedJSON(), 482
483 reporterAccount: this.ReporterAccount.toFormattedJSON(),
484
425 state: { 485 state: {
426 id: this.state, 486 id: this.state,
427 label: VideoAbuseModel.getStateLabel(this.state) 487 label: AbuseModel.getStateLabel(this.state)
428 }, 488 },
489
429 moderationComment: this.moderationComment, 490 moderationComment: this.moderationComment,
430 video: { 491
431 id: video.id, 492 video,
432 uuid: video.uuid, 493 comment: null,
433 name: video.name, 494
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, 495 createdAt: this.createdAt,
441 updatedAt: this.updatedAt, 496 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
444 count: countReportsForVideo || 0, 497 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0, 498 nth: nthReportForVideo || 0,
446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), 499 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
447 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) 500 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
501
502 // FIXME: deprecated in 2.3, remove this
503 startAt: null,
504 endAt: null
448 } 505 }
449 } 506 }
450 507
451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { 508 toActivityPubObject (this: MAbuseAP): AbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) 509 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
510
511 const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
453 512
454 const startAt = this.startAt 513 const startAt = this.VideoAbuse?.startAt
455 const endAt = this.endAt 514 const endAt = this.VideoAbuse?.endAt
456 515
457 return { 516 return {
458 type: 'Flag' as 'Flag', 517 type: 'Flag' as 'Flag',
459 content: this.reason, 518 content: this.reason,
460 object: this.Video.url, 519 object,
461 tag: predefinedReasons.map(r => ({ 520 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag', 521 type: 'Hashtag' as 'Hashtag',
463 name: r 522 name: r
@@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
468 } 527 }
469 528
470 private static getStateLabel (id: number) { 529 private static getStateLabel (id: number) {
471 return VIDEO_ABUSE_STATES[id] || 'Unknown' 530 return ABUSE_STATES[id] || 'Unknown'
472 } 531 }
473 532
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { 533 private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
475 return (predefinedReasons || []) 534 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons) 535 .filter(r => r in AbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) 536 .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
478 } 537 }
479} 538}
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..b4cc2762e
--- /dev/null
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -0,0 +1,53 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoComment } from '@shared/models'
3import { VideoCommentModel } from '../video/video-comment'
4import { AbuseModel } from './abuse'
5
6@Table({
7 tableName: 'commentAbuse',
8 indexes: [
9 {
10 fields: [ 'abuseId' ]
11 },
12 {
13 fields: [ 'videoCommentId' ]
14 }
15 ]
16})
17export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
18
19 @CreatedAt
20 createdAt: Date
21
22 @UpdatedAt
23 updatedAt: Date
24
25 @AllowNull(true)
26 @Default(null)
27 @Column(DataType.JSONB)
28 deletedComment: VideoComment
29
30 @ForeignKey(() => AbuseModel)
31 @Column
32 abuseId: number
33
34 @BelongsTo(() => AbuseModel, {
35 foreignKey: {
36 allowNull: false
37 },
38 onDelete: 'cascade'
39 })
40 Abuse: AbuseModel
41
42 @ForeignKey(() => VideoCommentModel)
43 @Column
44 videoCommentId: number
45
46 @BelongsTo(() => VideoCommentModel, {
47 foreignKey: {
48 allowNull: true
49 },
50 onDelete: 'set null'
51 })
52 VideoComment: VideoCommentModel
53}
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..466d6258e 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -388,6 +388,10 @@ export class AccountModel extends Model<AccountModel> {
388 .findAll(query) 388 .findAll(query)
389 } 389 }
390 390
391 getClientUrl () {
392 return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
393 }
394
391 toFormattedJSON (this: MAccountFormattable): Account { 395 toFormattedJSON (this: MAccountFormattable): Account {
392 const actor = this.Actor.toFormattedJSON() 396 const actor = this.Actor.toFormattedJSON()
393 const account = { 397 const account = {
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 30985bb0f..07db5a2db 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: [ '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,27 @@ 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 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
496 }
497 } : undefined
498
499 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
500
501 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
502
503 return {
504 id: abuse.id,
505 video: videoAbuse,
506 comment: commentAbuse,
507 account: accountAbuse
508 }
509 }
510
459 formatActor ( 511 formatActor (
460 this: UserNotificationModelForApi, 512 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor 513 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index de193131a..f21eff04b 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,
@@ -169,7 +169,7 @@ enum ScopeNames {
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("videoAbuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' + 173 'FROM "videoAbuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
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.ts b/server/models/video/video.ts
index e2718300e..272bba0e1 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',
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index 557bf20eb..f122baef4 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 () {
@@ -190,7 +191,7 @@ describe('Test video abuses API validators', function () {
190 }) 191 })
191 192
192 it('Should succeed with the correct params', async function () { 193 it('Should succeed with the correct params', async function () {
193 const body = { state: VideoAbuseState.ACCEPTED } 194 const body = { state: AbuseState.ACCEPTED }
194 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body) 195 await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
195 }) 196 })
196 }) 197 })
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 0a66bd1ce..88b68d977 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index' 5import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models'
6import { 6import {
7 addVideoCommentThread, 7 addVideoCommentThread,
8 blockUser, 8 blockUser,
@@ -937,7 +937,7 @@ describe('Test users', function () {
937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations 937 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
938 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created 938 expect(user2.videoAbusesCreatedCount).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 updateVideoAbuse(server.url, server.accessToken, videoId, 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)
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 7383bd991..20975aa4a 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,7 +97,7 @@ 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)
@@ -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.count).to.equal(1)
140 expect(abuse1.nth).to.equal(1) 142 expect(abuse1.nth).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,7 +279,7 @@ 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.count).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.nth).to.equal(1, "wrong report position in report list for video 3")
@@ -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..abbc93d6f
--- /dev/null
+++ b/server/types/models/moderation/abuse.ts
@@ -0,0 +1,97 @@
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 } 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
54// ############################################################################
55
56export type MAbuseId = Pick<AbuseModel, 'id'>
57
58export type MAbuseVideo =
59 MAbuse &
60 Pick<AbuseModel, 'toActivityPubObject'> &
61 Use<'VideoAbuse', MVideoAbuseVideo>
62
63export type MAbuseUrl =
64 MAbuse &
65 Use<'VideoAbuse', MVideoAbuseVideoUrl> &
66 Use<'VideoCommentAbuse', MCommentAbuseUrl>
67
68export type MAbuseAccountVideo =
69 MAbuse &
70 Pick<AbuseModel, 'toActivityPubObject'> &
71 Use<'VideoAbuse', MVideoAbuseVideoFull> &
72 Use<'ReporterAccount', MAccountDefault>
73
74export type MAbuseAP =
75 MAbuse &
76 Pick<AbuseModel, 'toActivityPubObject'> &
77 Use<'ReporterAccount', MAccountUrl> &
78 Use<'FlaggedAccount', MAccountUrl> &
79 Use<'VideoAbuse', MVideoAbuseVideo> &
80 Use<'VideoCommentAbuse', MCommentAbuseAccount>
81
82export type MAbuseFull =
83 MAbuse &
84 Pick<AbuseModel, 'toActivityPubObject'> &
85 Use<'ReporterAccount', MAccountLight> &
86 Use<'FlaggedAccount', MAccountLight> &
87 Use<'VideoAbuse', MVideoAbuseVideoFull> &
88 Use<'VideoCommentAbuse', MCommentAbuseAccountVideo>
89
90// ############################################################################
91
92// Format for API or AP object
93
94export type MAbuseFormattable =
95 MAbuse &
96 Use<'ReporterAccount', MAccountFormattable> &
97 Use<'VideoAbuse', MVideoAbuseFormattable>
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..92ea16768 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, '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