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