aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html4
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts2
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html2
-rw-r--r--client/src/app/shared/shared-moderation/video-report.component.ts1
-rw-r--r--server/controllers/api/abuse.ts4
-rw-r--r--server/controllers/api/users/my-history.ts2
-rw-r--r--server/helpers/custom-validators/abuses.ts6
-rw-r--r--server/helpers/custom-validators/video-comments.ts4
-rw-r--r--server/helpers/middlewares/abuses.ts2
-rw-r--r--server/initializers/migrations/0520-abuses-split.ts2
-rw-r--r--server/lib/emailer.ts1
-rw-r--r--server/lib/emails/video-comment-abuse-new/html.pug3
-rw-r--r--server/lib/notifier.ts2
-rw-r--r--server/middlewares/validators/abuse.ts2
-rw-r--r--server/models/abuse/abuse.ts10
-rw-r--r--server/models/abuse/video-comment-abuse.ts8
-rw-r--r--server/models/account/user-notification.ts4
-rw-r--r--server/models/video/video-comment.ts42
-rw-r--r--server/tests/api/check-params/abuses.ts4
-rw-r--r--server/tests/api/moderation/abuses.ts931
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts73
-rw-r--r--server/tests/api/server/email.ts2
-rw-r--r--server/types/models/user/user-notification.ts2
-rw-r--r--server/typings/express/index.d.ts1
-rw-r--r--shared/extra-utils/moderation/abuses.ts14
-rw-r--r--shared/extra-utils/server/servers.ts4
-rw-r--r--shared/extra-utils/users/user-notifications.ts75
-rw-r--r--shared/models/users/user-notification.model.ts2
-rw-r--r--support/doc/api/openapi.yaml24
29 files changed, 858 insertions, 375 deletions
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 167f32fe6..333438269 100644
--- a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -138,8 +138,8 @@
138 <tr> 138 <tr>
139 <td colspan="6"> 139 <td colspan="6">
140 <div class="no-results"> 140 <div class="no-results">
141 <ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container> 141 <ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
142 <ng-container *ngIf="!search" i18n>No video abuses found.</ng-container> 142 <ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
143 </div> 143 </div>
144 </td> 144 </td>
145 </tr> 145 </tr>
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 389a242fd..a137f8c62 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -118,7 +118,7 @@ export class UserNotification implements UserNotificationServer {
118 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] 118 this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
119 break 119 break
120 120
121 case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: 121 case UserNotificationType.NEW_ABUSE_FOR_MODERATORS:
122 this.abuseUrl = '/admin/moderation/abuses/list' 122 this.abuseUrl = '/admin/moderation/abuses/list'
123 123
124 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video) 124 if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 8d31eab0d..2b341af2c 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 </ng-container> 43 </ng-container>
44 44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS">
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" i18n> 48 <div class="message" i18n>
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts
index b8d9f8d27..7977e4cca 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/video-report.component.ts
@@ -140,7 +140,6 @@ export class VideoReportComponent extends FormReactive implements OnInit {
140 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value 140 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
141 141
142 this.abuseService.reportVideo({ 142 this.abuseService.reportVideo({
143 accountId: this.video.account.id,
144 reason, 143 reason,
145 predefinedReasons, 144 predefinedReasons,
146 video: { 145 video: {
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
index 38808021d..04a0c06e3 100644
--- a/server/controllers/api/abuse.ts
+++ b/server/controllers/api/abuse.ts
@@ -100,7 +100,7 @@ async function updateAbuse (req: express.Request, res: express.Response) {
100 return abuse.save({ transaction: t }) 100 return abuse.save({ transaction: t })
101 }) 101 })
102 102
103 // Do not send the delete to other instances, we updated OUR copy of this video abuse 103 // Do not send the delete to other instances, we updated OUR copy of this abuse
104 104
105 return res.type('json').status(204).end() 105 return res.type('json').status(204).end()
106} 106}
@@ -112,7 +112,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
112 return abuse.destroy({ transaction: t }) 112 return abuse.destroy({ transaction: t })
113 }) 113 })
114 114
115 // Do not send the delete to other instances, we delete OUR copy of this video abuse 115 // Do not send the delete to other instances, we delete OUR copy of this abuse
116 116
117 return res.type('json').status(204).end() 117 return res.type('json').status(204).end()
118} 118}
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/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts
index c21468caa..0ca06a252 100644
--- a/server/helpers/custom-validators/abuses.ts
+++ b/server/helpers/custom-validators/abuses.ts
@@ -3,10 +3,10 @@ import { AbuseFilter, abusePredefinedReasonsMap, AbusePredefinedReasonsString, A
3import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' 3import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
4import { exists, isArray } from './misc' 4import { exists, isArray } from './misc'
5 5
6const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES 6const ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
7 7
8function isAbuseReasonValid (value: string) { 8function isAbuseReasonValid (value: string) {
9 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) 9 return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.REASON)
10} 10}
11 11
12function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) { 12function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
@@ -32,7 +32,7 @@ function isAbuseTimestampCoherent (endAt: number, { req }) {
32} 32}
33 33
34function isAbuseModerationCommentValid (value: string) { 34function isAbuseModerationCommentValid (value: string) {
35 return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) 35 return exists(value) && validator.isLength(value, ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
36} 36}
37 37
38function isAbuseStateValid (value: string) { 38function isAbuseStateValid (value: string) {
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
index a01680cbe..455ff4241 100644
--- a/server/helpers/custom-validators/video-comments.ts
+++ b/server/helpers/custom-validators/video-comments.ts
@@ -68,7 +68,7 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r
68 68
69async function doesCommentIdExist (idArg: number | string, res: express.Response) { 69async function doesCommentIdExist (idArg: number | string, res: express.Response) {
70 const id = parseInt(idArg + '', 10) 70 const id = parseInt(idArg + '', 10)
71 const videoComment = await VideoCommentModel.loadById(id) 71 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
72 72
73 if (!videoComment) { 73 if (!videoComment) {
74 res.status(404) 74 res.status(404)
@@ -77,7 +77,7 @@ async function doesCommentIdExist (idArg: number | string, res: express.Response
77 return false 77 return false
78 } 78 }
79 79
80 res.locals.videoComment = videoComment 80 res.locals.videoCommentFull = videoComment
81 81
82 return true 82 return true
83} 83}
diff --git a/server/helpers/middlewares/abuses.ts b/server/helpers/middlewares/abuses.ts
index b102273a2..be8c8b449 100644
--- a/server/helpers/middlewares/abuses.ts
+++ b/server/helpers/middlewares/abuses.ts
@@ -30,7 +30,7 @@ async function doesAbuseExist (abuseId: number | string, res: Response) {
30 30
31 if (!abuse) { 31 if (!abuse) {
32 res.status(404) 32 res.status(404)
33 .json({ error: 'Video abuse not found' }) 33 .json({ error: 'Abuse not found' })
34 34
35 return false 35 return false
36 } 36 }
diff --git a/server/initializers/migrations/0520-abuses-split.ts b/server/initializers/migrations/0520-abuses-split.ts
index 5898d501f..b02a21989 100644
--- a/server/initializers/migrations/0520-abuses-split.ts
+++ b/server/initializers/migrations/0520-abuses-split.ts
@@ -43,12 +43,10 @@ async function up (utils: {
43 await utils.sequelize.query(` 43 await utils.sequelize.query(`
44 CREATE TABLE IF NOT EXISTS "commentAbuse" ( 44 CREATE TABLE IF NOT EXISTS "commentAbuse" (
45 "id" serial, 45 "id" serial,
46 "deletedComment" jsonb DEFAULT NULL,
47 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 46 "abuseId" integer NOT NULL REFERENCES "abuse" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
48 "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE, 47 "videoCommentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
49 "createdAt" timestamp WITH time zone NOT NULL, 48 "createdAt" timestamp WITH time zone NOT NULL,
50 "updatedAt" timestamp WITH time zone NOT NULL, 49 "updatedAt" timestamp WITH time zone NOT NULL,
51 "commentId" integer REFERENCES "videoComment" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
52 PRIMARY KEY ("id") 50 PRIMARY KEY ("id")
53 ); 51 );
54 `) 52 `)
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index a5664408d..5a6f37bb9 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -325,6 +325,7 @@ class Emailer {
325 subject: `New comment abuse report from ${reporter}`, 325 subject: `New comment abuse report from ${reporter}`,
326 locals: { 326 locals: {
327 commentUrl, 327 commentUrl,
328 videoName: comment.Video.name,
328 isLocal: comment.isOwned(), 329 isLocal: comment.isOwned(),
329 commentCreatedAt: new Date(comment.createdAt).toLocaleString(), 330 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
330 reason: abuse.reason, 331 reason: abuse.reason,
diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug
index 170b79576..fc1c3e4e7 100644
--- a/server/lib/emails/video-comment-abuse-new/html.pug
+++ b/server/lib/emails/video-comment-abuse-new/html.pug
@@ -7,7 +7,8 @@ block title
7block content 7block content
8 p 8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment " 9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment "
10 a(href=commentUrl) of #{flaggedAccount} 10 a(href=commentUrl) on video #{videoName}
11 | of #{flaggedAccount}
11 | created on #{commentCreatedAt} 12 | created on #{commentCreatedAt}
12 13
13 p The reporter, #{reporter}, cited the following reason(s): 14 p The reporter, #{reporter}, cited the following reason(s):
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 969e393fa..c567e1c20 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -371,7 +371,7 @@ class Notifier {
371 371
372 async function notificationCreator (user: MUserWithNotificationSetting) { 372 async function notificationCreator (user: MUserWithNotificationSetting) {
373 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ 373 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
374 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 374 type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS,
375 userId: user.id, 375 userId: user.id,
376 abuseId: abuse.id 376 abuseId: abuse.id
377 }) 377 })
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
index 048dbead0..966d1f7fb 100644
--- a/server/middlewares/validators/abuse.ts
+++ b/server/middlewares/validators/abuse.ts
@@ -128,7 +128,7 @@ const abuseListValidator = [
128 .custom(exists).withMessage('Should have a valid search'), 128 .custom(exists).withMessage('Should have a valid search'),
129 query('state') 129 query('state')
130 .optional() 130 .optional()
131 .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'), 131 .custom(isAbuseStateValid).withMessage('Should have a valid abuse state'),
132 query('videoIs') 132 query('videoIs')
133 .optional() 133 .optional()
134 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'), 134 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
index 28ecf8253..dffd503b3 100644
--- a/server/models/abuse/abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -362,8 +362,8 @@ export class AbuseModel extends Model<AbuseModel> {
362 const countReportsForReporter = this.get('countReportsForReporter') as number 362 const countReportsForReporter = this.get('countReportsForReporter') as number
363 const countReportsForReportee = this.get('countReportsForReportee') as number 363 const countReportsForReportee = this.get('countReportsForReportee') as number
364 364
365 let video: VideoAbuse 365 let video: VideoAbuse = null
366 let comment: VideoCommentAbuse 366 let comment: VideoCommentAbuse = null
367 367
368 if (this.VideoAbuse) { 368 if (this.VideoAbuse) {
369 const abuseModel = this.VideoAbuse 369 const abuseModel = this.VideoAbuse
@@ -391,13 +391,13 @@ export class AbuseModel extends Model<AbuseModel> {
391 391
392 if (this.VideoCommentAbuse) { 392 if (this.VideoCommentAbuse) {
393 const abuseModel = this.VideoCommentAbuse 393 const abuseModel = this.VideoCommentAbuse
394 const entity = abuseModel.VideoComment || abuseModel.deletedComment 394 const entity = abuseModel.VideoComment
395 395
396 comment = { 396 comment = {
397 id: entity.id, 397 id: entity.id,
398 text: entity.text, 398 text: entity.text ?? '',
399 399
400 deleted: !abuseModel.VideoComment, 400 deleted: entity.isDeleted(),
401 401
402 video: { 402 video: {
403 id: entity.Video.id, 403 id: entity.Video.id,
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts
index de9f4d5fd..8b34009b4 100644
--- a/server/models/abuse/video-comment-abuse.ts
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -1,5 +1,4 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoComment } from '@shared/models'
3import { VideoCommentModel } from '../video/video-comment' 2import { VideoCommentModel } from '../video/video-comment'
4import { AbuseModel } from './abuse' 3import { AbuseModel } from './abuse'
5 4
@@ -22,11 +21,6 @@ export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
22 @UpdatedAt 21 @UpdatedAt
23 updatedAt: Date 22 updatedAt: Date
24 23
25 @AllowNull(true)
26 @Default(null)
27 @Column(DataType.JSONB)
28 deletedComment: VideoComment & { Video: { name: string, id: number, uuid: string }}
29
30 @ForeignKey(() => AbuseModel) 24 @ForeignKey(() => AbuseModel)
31 @Column 25 @Column
32 abuseId: number 26 abuseId: number
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 07db5a2db..2945bf709 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -109,7 +109,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
109 required: true, 109 required: true,
110 include: [ 110 include: [
111 { 111 {
112 attributes: [ 'uuid' ], 112 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(), 113 model: VideoModel.unscoped(),
114 required: true 114 required: true
115 } 115 }
@@ -492,6 +492,8 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
492 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), 492 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
493 493
494 video: { 494 video: {
495 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
496 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
495 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid 497 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
496 } 498 }
497 } : undefined 499 } : undefined
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index fb6078ed8..fa4d13c3b 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -3,7 +3,6 @@ import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' 3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
4import { 4import {
5 AllowNull, 5 AllowNull,
6 BeforeDestroy,
7 BelongsTo, 6 BelongsTo,
8 Column, 7 Column,
9 CreatedAt, 8 CreatedAt,
@@ -16,7 +15,6 @@ import {
16 Table, 15 Table,
17 UpdatedAt 16 UpdatedAt
18} from 'sequelize-typescript' 17} from 'sequelize-typescript'
19import { logger } from '@server/helpers/logger'
20import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
21import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 19import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
22import { VideoPrivacy } from '@shared/models' 20import { VideoPrivacy } from '@shared/models'
@@ -242,51 +240,13 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
242 240
243 @HasMany(() => VideoCommentAbuseModel, { 241 @HasMany(() => VideoCommentAbuseModel, {
244 foreignKey: { 242 foreignKey: {
245 name: 'commentId', 243 name: 'videoCommentId',
246 allowNull: true 244 allowNull: true
247 }, 245 },
248 onDelete: 'set null' 246 onDelete: 'set null'
249 }) 247 })
250 CommentAbuses: VideoCommentAbuseModel[] 248 CommentAbuses: VideoCommentAbuseModel[]
251 249
252 @BeforeDestroy
253 static async saveEssentialDataToAbuses (instance: VideoCommentModel, options) {
254 const tasks: Promise<any>[] = []
255
256 if (!Array.isArray(instance.CommentAbuses)) {
257 instance.CommentAbuses = await instance.$get('CommentAbuses')
258
259 if (instance.CommentAbuses.length === 0) return undefined
260 }
261
262 if (!instance.Video) {
263 instance.Video = await instance.$get('Video')
264 }
265
266 logger.info('Saving video comment %s for abuse.', instance.url)
267
268 const details = Object.assign(instance.toFormattedJSON(), {
269 Video: {
270 id: instance.Video.id,
271 name: instance.Video.name,
272 uuid: instance.Video.uuid
273 }
274 })
275
276 for (const abuse of instance.CommentAbuses) {
277 abuse.deletedComment = details
278
279 tasks.push(abuse.save({ transaction: options.transaction }))
280 }
281
282 Promise.all(tasks)
283 .catch(err => {
284 logger.error('Some errors when saving details of comment %s in its abuses before destroy hook.', instance.url, { err })
285 })
286
287 return undefined
288 }
289
290 static loadById (id: number, t?: Transaction): Bluebird<MComment> { 250 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
291 const query: FindOptions = { 251 const query: FindOptions = {
292 where: { 252 where: {
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts
index ba7c0833f..8964c0ab2 100644
--- a/server/tests/api/check-params/abuses.ts
+++ b/server/tests/api/check-params/abuses.ts
@@ -21,9 +21,7 @@ import {
21 checkBadStartPagination 21 checkBadStartPagination
22} from '../../../../shared/extra-utils/requests/check-api-params' 22} from '../../../../shared/extra-utils/requests/check-api-params'
23 23
24// FIXME: deprecated in 2.3. Remove this controller 24describe('Test abuses API validators', function () {
25
26describe('Test video abuses API validators', function () {
27 const basePath = '/api/v1/abuses/' 25 const basePath = '/api/v1/abuses/'
28 26
29 let server: ServerInfo 27 let server: ServerInfo
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts
index 28c5a5531..f186f7ea0 100644
--- a/server/tests/api/moderation/abuses.ts
+++ b/server/tests/api/moderation/abuses.ts
@@ -2,21 +2,30 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models' 5import { Abuse, AbuseFilter, AbusePredefinedReasonsString, AbuseState, VideoComment, Account } from '@shared/models'
6import { 6import {
7 addVideoCommentThread,
7 cleanupTests, 8 cleanupTests,
8 createUser, 9 createUser,
9 deleteVideoAbuse, 10 deleteAbuse,
11 deleteVideoComment,
10 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
11 getVideoAbusesList, 13 getAbusesList,
14 getVideoCommentThreads,
15 getVideoIdFromUUID,
12 getVideosList, 16 getVideosList,
17 immutableAssign,
13 removeVideo, 18 removeVideo,
14 reportVideoAbuse, 19 reportAbuse,
15 ServerInfo, 20 ServerInfo,
16 setAccessTokensToServers, 21 setAccessTokensToServers,
17 updateVideoAbuse, 22 updateAbuse,
18 uploadVideo, 23 uploadVideo,
19 userLogin 24 uploadVideoAndGetId,
25 userLogin,
26 getAccount,
27 removeUser,
28 generateUserAccessToken
20} from '../../../../shared/extra-utils/index' 29} from '../../../../shared/extra-utils/index'
21import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 30import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
22import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 31import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -31,6 +40,7 @@ const expect = chai.expect
31 40
32describe('Test abuses', function () { 41describe('Test abuses', function () {
33 let servers: ServerInfo[] = [] 42 let servers: ServerInfo[] = []
43 let abuseServer1: Abuse
34 let abuseServer2: Abuse 44 let abuseServer2: Abuse
35 45
36 before(async function () { 46 before(async function () {
@@ -44,338 +54,721 @@ describe('Test abuses', function () {
44 54
45 // Server 1 and server 2 follow each other 55 // Server 1 and server 2 follow each other
46 await doubleFollow(servers[0], servers[1]) 56 await doubleFollow(servers[0], servers[1])
57 })
47 58
48 // Upload some videos on each servers 59 describe('Video abuses', function () {
49 const video1Attributes = {
50 name: 'my super name for server 1',
51 description: 'my super description for server 1'
52 }
53 await uploadVideo(servers[0].url, servers[0].accessToken, video1Attributes)
54 60
55 const video2Attributes = { 61 before(async function () {
56 name: 'my super name for server 2', 62 this.timeout(50000)
57 description: 'my super description for server 2'
58 }
59 await uploadVideo(servers[1].url, servers[1].accessToken, video2Attributes)
60 63
61 // Wait videos propagation, server 2 has transcoding enabled 64 // Upload some videos on each servers
62 await waitJobs(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)
63 70
64 const res = await getVideosList(servers[0].url) 71 const video2Attributes = {
65 const videos = res.body.data 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)
66 76
67 expect(videos.length).to.equal(2) 77 // Wait videos propagation, server 2 has transcoding enabled
78 await waitJobs(servers)
68 79
69 servers[0].video = videos.find(video => video.name === 'my super name for server 1') 80 const res = await getVideosList(servers[0].url)
70 servers[1].video = videos.find(video => video.name === 'my super name for server 2') 81 const videos = res.body.data
71 })
72 82
73 it('Should not have video abuses', async function () { 83 expect(videos.length).to.equal(2)
74 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
75 84
76 expect(res.body.total).to.equal(0) 85 servers[0].video = videos.find(video => video.name === 'my super name for server 1')
77 expect(res.body.data).to.be.an('array') 86 servers[1].video = videos.find(video => video.name === 'my super name for server 2')
78 expect(res.body.data.length).to.equal(0) 87 })
79 })
80 88
81 it('Should report abuse on a local video', async function () { 89 it('Should not have abuses', async function () {
82 this.timeout(15000) 90 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
83 91
84 const reason = 'my super bad reason' 92 expect(res.body.total).to.equal(0)
85 await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[0].video.id, reason) 93 expect(res.body.data).to.be.an('array')
94 expect(res.body.data.length).to.equal(0)
95 })
86 96
87 // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2 97 it('Should report abuse on a local video', async function () {
88 await waitJobs(servers) 98 this.timeout(15000)
89 })
90 99
91 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { 100 const reason = 'my super bad reason'
92 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 101 await reportAbuse({ url: servers[0].url, token: servers[0].accessToken, videoId: servers[0].video.id, reason })
93
94 expect(res1.body.total).to.equal(1)
95 expect(res1.body.data).to.be.an('array')
96 expect(res1.body.data.length).to.equal(1)
97
98 const abuse: Abuse = res1.body.data[0]
99 expect(abuse.reason).to.equal('my super bad reason')
100 expect(abuse.reporterAccount.name).to.equal('root')
101 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
102 expect(abuse.video.id).to.equal(servers[0].video.id)
103 expect(abuse.video.channel).to.exist
104 expect(abuse.count).to.equal(1)
105 expect(abuse.nth).to.equal(1)
106 expect(abuse.countReportsForReporter).to.equal(1)
107 expect(abuse.countReportsForReportee).to.equal(1)
108
109 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
110 expect(res2.body.total).to.equal(0)
111 expect(res2.body.data).to.be.an('array')
112 expect(res2.body.data.length).to.equal(0)
113 })
114 102
115 it('Should report abuse on a remote video', async function () { 103 // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
116 this.timeout(10000) 104 await waitJobs(servers)
105 })
117 106
118 const reason = 'my super bad reason 2' 107 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
119 await reportVideoAbuse(servers[0].url, servers[0].accessToken, servers[1].video.id, reason) 108 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
120 109
121 // We wait requests propagation 110 expect(res1.body.total).to.equal(1)
122 await waitJobs(servers) 111 expect(res1.body.data).to.be.an('array')
123 }) 112 expect(res1.body.data.length).to.equal(1)
124 113
125 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { 114 const abuse: Abuse = res1.body.data[0]
126 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 115 expect(abuse.reason).to.equal('my super bad reason')
127 expect(res1.body.total).to.equal(2)
128 expect(res1.body.data).to.be.an('array')
129 expect(res1.body.data.length).to.equal(2)
130
131 const abuse1: Abuse = res1.body.data[0]
132 expect(abuse1.reason).to.equal('my super bad reason')
133 expect(abuse1.reporterAccount.name).to.equal('root')
134 expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
135 expect(abuse1.video.id).to.equal(servers[0].video.id)
136 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
137 expect(abuse1.state.label).to.equal('Pending')
138 expect(abuse1.moderationComment).to.be.null
139 expect(abuse1.count).to.equal(1)
140 expect(abuse1.nth).to.equal(1)
141
142 const abuse2: Abuse = res1.body.data[1]
143 expect(abuse2.reason).to.equal('my super bad reason 2')
144 expect(abuse2.reporterAccount.name).to.equal('root')
145 expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
146 expect(abuse2.video.id).to.equal(servers[1].video.id)
147 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
148 expect(abuse2.state.label).to.equal('Pending')
149 expect(abuse2.moderationComment).to.be.null
150
151 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
152 expect(res2.body.total).to.equal(1)
153 expect(res2.body.data).to.be.an('array')
154 expect(res2.body.data.length).to.equal(1)
155
156 abuseServer2 = res2.body.data[0]
157 expect(abuseServer2.reason).to.equal('my super bad reason 2')
158 expect(abuseServer2.reporterAccount.name).to.equal('root')
159 expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
160 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
161 expect(abuseServer2.state.label).to.equal('Pending')
162 expect(abuseServer2.moderationComment).to.be.null
163 })
164 116
165 it('Should update the state of a video abuse', async function () { 117 expect(abuse.reporterAccount.name).to.equal('root')
166 const body = { state: AbuseState.REJECTED } 118 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
168 119
169 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) 120 expect(abuse.video.id).to.equal(servers[0].video.id)
170 expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED) 121 expect(abuse.video.channel).to.exist
171 })
172 122
173 it('Should add a moderation comment', async function () { 123 expect(abuse.comment).to.be.null
174 const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
176 124
177 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) 125 expect(abuse.flaggedAccount.name).to.equal('root')
178 expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED) 126 expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
179 expect(res.body.data[0].moderationComment).to.equal('It is valid') 127
180 }) 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)
181 133
182 it('Should hide video abuses from blocked accounts', async function () { 134 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
183 this.timeout(10000) 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 })
184 139
185 { 140 it('Should report abuse on a remote video', async function () {
186 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this') 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
187 await waitJobs(servers) 147 await waitJobs(servers)
148 })
188 149
189 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 150 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
190 expect(res.body.total).to.equal(3) 151 const res1 = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
191 }
192 152
193 const accountToBlock = 'root@localhost:' + servers[1].port 153 expect(res1.body.total).to.equal(2)
154 expect(res1.body.data.length).to.equal(2)
194 155
195 { 156 const abuse1: Abuse = res1.body.data[0]
196 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) 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)
197 160
198 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 161 expect(abuse1.video.id).to.equal(servers[0].video.id)
199 expect(res.body.total).to.equal(2) 162 expect(abuse1.video.countReports).to.equal(1)
163 expect(abuse1.video.nthReport).to.equal(1)
200 164
201 const abuse = res.body.data.find(a => a.reason === 'will mute this') 165 expect(abuse1.comment).to.be.null
202 expect(abuse).to.be.undefined
203 }
204 166
205 { 167 expect(abuse1.flaggedAccount.name).to.equal('root')
206 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock) 168 expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
207 169
208 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 170 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
209 expect(res.body.total).to.equal(3) 171 expect(abuse1.state.label).to.equal('Pending')
210 } 172 expect(abuse1.moderationComment).to.be.null
211 })
212 173
213 it('Should hide video abuses from blocked servers', async function () { 174 const abuse2: Abuse = res1.body.data[1]
214 const serverToBlock = servers[1].host 175 expect(abuse2.reason).to.equal('my super bad reason 2')
215 176
216 { 177 expect(abuse2.reporterAccount.name).to.equal('root')
217 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) 178 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
218 179
219 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 180 expect(abuse2.video.id).to.equal(servers[1].video.id)
220 expect(res.body.total).to.equal(2)
221 181
222 const abuse = res.body.data.find(a => a.reason === 'will mute this') 182 expect(abuse2.comment).to.be.null
223 expect(abuse).to.be.undefined
224 }
225 183
226 { 184 expect(abuse2.flaggedAccount.name).to.equal('root')
227 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock) 185 expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
228 186
229 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 187 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
230 expect(res.body.total).to.equal(3) 188 expect(abuse2.state.label).to.equal('Pending')
231 } 189 expect(abuse2.moderationComment).to.be.null
232 })
233 190
234 it('Should keep the video abuse when deleting the video', async function () { 191 const res2 = await getAbusesList({ url: servers[1].url, token: servers[1].accessToken })
235 this.timeout(10000) 192 expect(res2.body.total).to.equal(1)
193 expect(res2.body.data.length).to.equal(1)
236 194
237 await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid) 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)
238 199
239 await waitJobs(servers) 200 expect(abuse2.flaggedAccount.name).to.equal('root')
201 expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
240 202
241 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken }) 203 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
242 expect(res.body.total).to.equal(2, "wrong number of videos returned") 204 expect(abuseServer2.state.label).to.equal('Pending')
243 expect(res.body.data.length).to.equal(2, "wrong number of videos returned") 205 expect(abuseServer2.moderationComment).to.be.null
244 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video") 206 })
245 207
246 const abuse: Abuse = res.body.data[0] 208 it('Should hide video abuses from blocked accounts', async function () {
247 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id") 209 this.timeout(10000)
248 expect(abuse.video.channel).to.exist
249 expect(abuse.video.deleted).to.be.true
250 })
251 210
252 it('Should include counts of reports from reporter and reportee', async function () { 211 {
253 this.timeout(10000) 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)
254 215
255 // register a second user to have two reporters/reportees 216 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
256 const user = { username: 'user2', password: 'password' } 217 expect(res.body.total).to.equal(3)
257 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user }) 218 }
258 const userAccessToken = await userLogin(servers[0], user)
259 219
260 // upload a third video via this user 220 const accountToBlock = 'root@' + servers[1].host
261 const video3Attributes = { 221
262 name: 'my second super name for server 1', 222 {
263 description: 'my second super description for server 1' 223 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
264 } 224
265 await uploadVideo(servers[0].url, userAccessToken, video3Attributes) 225 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
266 226 expect(res.body.total).to.equal(2)
267 const res1 = await getVideosList(servers[0].url) 227
268 const videos = res1.body.data 228 const abuse = res.body.data.find(a => a.reason === 'will mute this')
269 const video3 = videos.find(video => video.name === 'my second super name for server 1') 229 expect(abuse).to.be.undefined
270 230 }
271 // resume with the test 231
272 const reason3 = 'my super bad reason 3' 232 {
273 await reportVideoAbuse(servers[0].url, servers[0].accessToken, video3.id, reason3) 233 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
274 const reason4 = 'my super bad reason 4' 234
275 await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4) 235 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
276 236 expect(res.body.total).to.equal(3)
277 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 237 }
278 238 })
279 { 239
280 for (const abuse of res2.body.data as Abuse[]) { 240 it('Should hide video abuses from blocked servers', async function () {
281 if (abuse.video.id === video3.id) { 241 const serverToBlock = servers[1].host
282 expect(abuse.count).to.equal(1, "wrong reports count for video 3") 242
283 expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3") 243 {
284 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse") 244 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
285 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse") 245
286 } 246 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken })
287 if (abuse.video.id === servers[0].video.id) { 247 expect(res.body.total).to.equal(2)
288 expect(abuse.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse") 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
289 } 374 }
375
376 Object.assign(options, query)
377
378 const res = await getAbusesList(options)
379
380 return res.body.data as Abuse[]
290 } 381 }
291 }
292 })
293 382
294 it('Should list predefined reasons as well as timestamps for the reported video', async function () { 383 expect(await list({ id: 56 })).to.have.lengthOf(0)
295 this.timeout(10000) 384 expect(await list({ id: 1 })).to.have.lengthOf(1)
296 385
297 const reason5 = 'my super bad reason 5' 386 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
298 const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ] 387 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
299 const createdAbuse = (await reportVideoAbuse( 388
300 servers[0].url, 389 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
301 servers[0].accessToken, 390
302 servers[0].video.id, 391 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
303 reason5, 392 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
304 predefinedReasons5, 393
305 1, 394 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
306 5 395 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
307 )).body.abuse 396
308 397 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 398 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
310 399
311 { 400 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
312 const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id) 401 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
313 expect(abuse.reason).to.equals(reason5) 402
314 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported") 403 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
315 expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported") 404 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
316 expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported") 405
317 } 406 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
407 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
408 })
318 }) 409 })
319 410
320 it('Should delete the video abuse', async function () { 411 describe('Comment abuses', function () {
321 this.timeout(10000)
322 412
323 await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id) 413 async function getComment (url: string, videoIdArg: number | string) {
414 const videoId = typeof videoIdArg === 'string'
415 ? await getVideoIdFromUUID(url, videoIdArg)
416 : videoIdArg
324 417
325 await waitJobs(servers) 418 const res = await getVideoCommentThreads(url, videoId, 0, 5)
326 419
327 { 420 return res.body.data[0] as VideoComment
328 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
329 expect(res.body.total).to.equal(1)
330 expect(res.body.data.length).to.equal(1)
331 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
332 } 421 }
333 422
334 { 423 before(async function () {
335 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken }) 424 this.timeout(50000)
336 expect(res.body.total).to.equal(6)
337 }
338 })
339 425
340 it('Should list and filter video abuses', async function () { 426 servers[0].video = await uploadVideoAndGetId({ server: servers[0], videoName: 'server 1' })
341 async function list (query: Omit<Parameters<typeof getVideoAbusesList>[0], 'url' | 'token'>) { 427 servers[1].video = await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })
342 const options = { 428
343 url: servers[0].url, 429 await addVideoCommentThread(servers[0].url, servers[0].accessToken, servers[0].video.id, 'comment server 1')
344 token: servers[0].accessToken 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')
345 } 605 }
606 })
607 })
346 608
347 Object.assign(options, query) 609 describe('Account abuses', function () {
348 610
349 const res = await getVideoAbusesList(options) 611 async function getAccountFromServer (url: string, name: string, server: ServerInfo) {
612 const res = await getAccount(url, name + '@' + server.host)
350 613
351 return res.body.data as Abuse[] 614 return res.body as Account
352 } 615 }
353 616
354 expect(await list({ id: 56 })).to.have.lengthOf(0) 617 before(async function () {
355 expect(await list({ id: 1 })).to.have.lengthOf(1) 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')
356 684
357 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4) 685 const abuse2: Abuse = res1.body.data[1]
358 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0) 686 expect(abuse2.reason).to.equal('it is a really bad account')
359 687
360 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1) 688 expect(abuse2.reporterAccount.name).to.equal('root')
689 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
361 690
362 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4) 691 expect(abuse2.video).to.be.null
363 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0) 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 })
364 753
365 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1) 754 describe('Common actions on abuses', function () {
366 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
367 755
368 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5) 756 it('Should update the state of an abuse', async function () {
369 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0) 757 const body = { state: AbuseState.REJECTED }
758 await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
370 759
371 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1) 760 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
372 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0) 761 expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
762 })
373 763
374 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0) 764 it('Should add a moderation comment', async function () {
375 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6) 765 const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
766 await updateAbuse(servers[0].url, servers[0].accessToken, abuseServer1.id, body)
376 767
377 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1) 768 const res = await getAbusesList({ url: servers[0].url, token: servers[0].accessToken, id: abuseServer1.id })
378 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0) 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 })
379 }) 772 })
380 773
381 after(async function () { 774 after(async function () {
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts
index a27681603..a8517600a 100644
--- a/server/tests/api/notifications/moderation-notifications.ts
+++ b/server/tests/api/notifications/moderation-notifications.ts
@@ -3,10 +3,16 @@
3import 'mocha' 3import 'mocha'
4import { v4 as uuidv4 } from 'uuid' 4import { v4 as uuidv4 } from 'uuid'
5import { 5import {
6 addVideoCommentThread,
6 addVideoToBlacklist, 7 addVideoToBlacklist,
7 cleanupTests, 8 cleanupTests,
9 createUser,
8 follow, 10 follow,
11 generateUserAccessToken,
12 getAccount,
9 getCustomConfig, 13 getCustomConfig,
14 getVideoCommentThreads,
15 getVideoIdFromUUID,
10 immutableAssign, 16 immutableAssign,
11 MockInstancesIndex, 17 MockInstancesIndex,
12 registerUser, 18 registerUser,
@@ -23,7 +29,9 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
23import { 29import {
24 checkAutoInstanceFollowing, 30 checkAutoInstanceFollowing,
25 CheckerBaseParams, 31 CheckerBaseParams,
32 checkNewAccountAbuseForModerators,
26 checkNewBlacklistOnMyVideo, 33 checkNewBlacklistOnMyVideo,
34 checkNewCommentAbuseForModerators,
27 checkNewInstanceFollower, 35 checkNewInstanceFollower,
28 checkNewVideoAbuseForModerators, 36 checkNewVideoAbuseForModerators,
29 checkNewVideoFromSubscription, 37 checkNewVideoFromSubscription,
@@ -91,11 +99,74 @@ describe('Test moderation notifications', function () {
91 99
92 await waitJobs(servers) 100 await waitJobs(servers)
93 101
94 await reportAbuse({ url: servers[1].url, token: servers[1].accessToken, videoId: video.id, reason: 'super reason' }) 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' })
95 104
96 await waitJobs(servers) 105 await waitJobs(servers)
97 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence') 106 await checkNewVideoAbuseForModerators(baseParams, video.uuid, name, 'presence')
98 }) 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' })
161
162 await waitJobs(servers)
163
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' })
166
167 await waitJobs(servers)
168 await checkNewAccountAbuseForModerators(baseParams, username, 'presence')
169 })
99 }) 170 })
100 171
101 describe('Video blacklist on my video', function () { 172 describe('Video blacklist on my video', function () {
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 9c3299618..b01a91d48 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -180,7 +180,7 @@ describe('Test emails', function () {
180 }) 180 })
181 }) 181 })
182 182
183 describe('When creating a video abuse', function () { 183 describe('When creating an abuse', function () {
184 it('Should send the notification email', async function () { 184 it('Should send the notification email', async function () {
185 this.timeout(10000) 185 this.timeout(10000)
186 186
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index 92ea16768..f59eb7260 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -53,7 +53,7 @@ export module UserNotificationIncludes {
53 Pick<VideoCommentAbuseModel, 'id'> & 53 Pick<VideoCommentAbuseModel, 'id'> &
54 PickWith<VideoCommentAbuseModel, 'VideoComment', 54 PickWith<VideoCommentAbuseModel, 'VideoComment',
55 Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> & 55 Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
56 PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'uuid'>>> 56 PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'id' | 'name' | 'uuid'>>>
57 57
58 export type AbuseInclude = 58 export type AbuseInclude =
59 Pick<AbuseModel, 'id'> & 59 Pick<AbuseModel, 'id'> &
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index b1afffcd4..7595e6d86 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -91,7 +91,6 @@ declare module 'express' {
91 91
92 accountVideoRate?: MAccountVideoRateAccountVideo 92 accountVideoRate?: MAccountVideoRateAccountVideo
93 93
94 videoComment?: MComment
95 videoCommentFull?: MCommentOwnerVideoReply 94 videoCommentFull?: MCommentOwnerVideoReply
96 videoCommentThread?: MComment 95 videoCommentThread?: MComment
97 96
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
index 1af703f92..62af9556e 100644
--- a/shared/extra-utils/moderation/abuses.ts
+++ b/shared/extra-utils/moderation/abuses.ts
@@ -57,10 +57,15 @@ function reportAbuse (options: {
57function getAbusesList (options: { 57function getAbusesList (options: {
58 url: string 58 url: string
59 token: string 59 token: string
60
61 start?: number
62 count?: number
63 sort?: string
64
60 id?: number 65 id?: number
61 predefinedReason?: AbusePredefinedReasonsString 66 predefinedReason?: AbusePredefinedReasonsString
62 search?: string 67 search?: string
63 filter?: AbuseFilter, 68 filter?: AbuseFilter
64 state?: AbuseState 69 state?: AbuseState
65 videoIs?: AbuseVideoIs 70 videoIs?: AbuseVideoIs
66 searchReporter?: string 71 searchReporter?: string
@@ -71,6 +76,9 @@ function getAbusesList (options: {
71 const { 76 const {
72 url, 77 url,
73 token, 78 token,
79 start,
80 count,
81 sort,
74 id, 82 id,
75 predefinedReason, 83 predefinedReason,
76 search, 84 search,
@@ -85,13 +93,15 @@ function getAbusesList (options: {
85 const path = '/api/v1/abuses' 93 const path = '/api/v1/abuses'
86 94
87 const query = { 95 const query = {
88 sort: 'createdAt',
89 id, 96 id,
90 predefinedReason, 97 predefinedReason,
91 search, 98 search,
92 state, 99 state,
93 filter, 100 filter,
94 videoIs, 101 videoIs,
102 start,
103 count,
104 sort: sort || 'createdAt',
95 searchReporter, 105 searchReporter,
96 searchReportee, 106 searchReportee,
97 searchVideo, 107 searchVideo,
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 0f883d839..994aac628 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -37,8 +37,8 @@ interface ServerInfo {
37 video?: { 37 video?: {
38 id: number 38 id: number
39 uuid: string 39 uuid: string
40 name: string 40 name?: string
41 account: { 41 account?: {
42 name: string 42 name: string
43 } 43 }
44 } 44 }
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index 4a5bc30fe..2061e3353 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -139,13 +139,17 @@ async function checkNotification (
139} 139}
140 140
141function checkVideo (video: any, videoName?: string, videoUUID?: string) { 141function checkVideo (video: any, videoName?: string, videoUUID?: string) {
142 expect(video.name).to.be.a('string') 142 if (videoName) {
143 expect(video.name).to.not.be.empty 143 expect(video.name).to.be.a('string')
144 if (videoName) expect(video.name).to.equal(videoName) 144 expect(video.name).to.not.be.empty
145 expect(video.name).to.equal(videoName)
146 }
145 147
146 expect(video.uuid).to.be.a('string') 148 if (videoUUID) {
147 expect(video.uuid).to.not.be.empty 149 expect(video.uuid).to.be.a('string')
148 if (videoUUID) expect(video.uuid).to.equal(videoUUID) 150 expect(video.uuid).to.not.be.empty
151 expect(video.uuid).to.equal(videoUUID)
152 }
149 153
150 expect(video.id).to.be.a('number') 154 expect(video.id).to.be.a('number')
151} 155}
@@ -436,7 +440,7 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string,
436} 440}
437 441
438async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { 442async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
439 const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS 443 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
440 444
441 function notificationChecker (notification: UserNotification, type: CheckerType) { 445 function notificationChecker (notification: UserNotification, type: CheckerType) {
442 if (type === 'presence') { 446 if (type === 'presence') {
@@ -460,6 +464,56 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
460 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 464 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
461} 465}
462 466
467async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
468 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
469
470 function notificationChecker (notification: UserNotification, type: CheckerType) {
471 if (type === 'presence') {
472 expect(notification).to.not.be.undefined
473 expect(notification.type).to.equal(notificationType)
474
475 expect(notification.abuse.id).to.be.a('number')
476 checkVideo(notification.abuse.comment.video, videoName, videoUUID)
477 } else {
478 expect(notification).to.satisfy((n: UserNotification) => {
479 return n === undefined || n.abuse === undefined || n.abuse.comment.video.uuid !== videoUUID
480 })
481 }
482 }
483
484 function emailNotificationFinder (email: object) {
485 const text = email['text']
486 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
487 }
488
489 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
490}
491
492async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displayName: string, type: CheckerType) {
493 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
494
495 function notificationChecker (notification: UserNotification, type: CheckerType) {
496 if (type === 'presence') {
497 expect(notification).to.not.be.undefined
498 expect(notification.type).to.equal(notificationType)
499
500 expect(notification.abuse.id).to.be.a('number')
501 expect(notification.abuse.account.displayName).to.equal(displayName)
502 } else {
503 expect(notification).to.satisfy((n: UserNotification) => {
504 return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName
505 })
506 }
507 }
508
509 function emailNotificationFinder (email: object) {
510 const text = email['text']
511 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
512 }
513
514 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
515}
516
463async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { 517async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
464 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS 518 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
465 519
@@ -541,6 +595,9 @@ async function prepareNotificationsTest (serversCount = 3) {
541 smtp: { 595 smtp: {
542 hostname: 'localhost', 596 hostname: 'localhost',
543 port 597 port
598 },
599 signup: {
600 limit: 20
544 } 601 }
545 } 602 }
546 const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) 603 const servers = await flushAndRunMultipleServers(serversCount, overrideConfig)
@@ -623,5 +680,7 @@ export {
623 markAsReadNotifications, 680 markAsReadNotifications,
624 getLastNotification, 681 getLastNotification,
625 checkNewInstanceFollower, 682 checkNewInstanceFollower,
626 prepareNotificationsTest 683 prepareNotificationsTest,
684 checkNewCommentAbuseForModerators,
685 checkNewAccountAbuseForModerators
627} 686}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index 39090f5a1..11d96fd50 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -3,7 +3,7 @@ import { FollowState } from '../actors'
3export enum UserNotificationType { 3export enum UserNotificationType {
4 NEW_VIDEO_FROM_SUBSCRIPTION = 1, 4 NEW_VIDEO_FROM_SUBSCRIPTION = 1,
5 NEW_COMMENT_ON_MY_VIDEO = 2, 5 NEW_COMMENT_ON_MY_VIDEO = 2,
6 NEW_VIDEO_ABUSE_FOR_MODERATORS = 3, 6 NEW_ABUSE_FOR_MODERATORS = 3,
7 7
8 BLACKLIST_ON_MY_VIDEO = 4, 8 BLACKLIST_ON_MY_VIDEO = 4,
9 UNBLACKLIST_ON_MY_VIDEO = 5, 9 UNBLACKLIST_ON_MY_VIDEO = 5,
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 03e60925b..3b381bbb5 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -106,9 +106,9 @@ tags:
106 Managing plugins installed from a local path or from NPM, or search for new ones. 106 Managing plugins installed from a local path or from NPM, or search for new ones.
107 externalDocs: 107 externalDocs:
108 url: https://docs.joinpeertube.org/#/api-plugins 108 url: https://docs.joinpeertube.org/#/api-plugins
109 - name: Video Abuses 109 - name: Abuses
110 description: | 110 description: |
111 Video abuses deal with reports of local or remote videos alike. 111 Abuses deal with reports of local or remote videos/comments/accounts alike.
112 - name: Video 112 - name: Video
113 description: | 113 description: |
114 Operations dealing with listing, uploading, fetching or modifying videos. 114 Operations dealing with listing, uploading, fetching or modifying videos.
@@ -166,7 +166,7 @@ x-tagGroups:
166 - Search 166 - Search
167 - name: Moderation 167 - name: Moderation
168 tags: 168 tags:
169 - Video Abuses 169 - Abuses
170 - Video Blocks 170 - Video Blocks
171 - Account Blocks 171 - Account Blocks
172 - Server Blocks 172 - Server Blocks
@@ -1474,13 +1474,13 @@ paths:
1474 /videos/abuse: 1474 /videos/abuse:
1475 get: 1475 get:
1476 deprecated: true 1476 deprecated: true
1477 summary: List video abuses 1477 summary: List abuses
1478 security: 1478 security:
1479 - OAuth2: 1479 - OAuth2:
1480 - admin 1480 - admin
1481 - moderator 1481 - moderator
1482 tags: 1482 tags:
1483 - Video Abuses 1483 - Abuses
1484 parameters: 1484 parameters:
1485 - name: id 1485 - name: id
1486 in: query 1486 in: query
@@ -1508,7 +1508,7 @@ paths:
1508 type: string 1508 type: string
1509 - name: state 1509 - name: state
1510 in: query 1510 in: query
1511 description: 'The video playlist privacy (Pending = `1`, Rejected = `2`, Accepted = `3`)' 1511 description: 'The abuse state (Pending = `1`, Rejected = `2`, Accepted = `3`)'
1512 schema: 1512 schema:
1513 type: integer 1513 type: integer
1514 enum: 1514 enum:
@@ -1554,7 +1554,7 @@ paths:
1554 security: 1554 security:
1555 - OAuth2: [] 1555 - OAuth2: []
1556 tags: 1556 tags:
1557 - Video Abuses 1557 - Abuses
1558 - Videos 1558 - Videos
1559 parameters: 1559 parameters:
1560 - $ref: '#/components/parameters/idOrUUID' 1560 - $ref: '#/components/parameters/idOrUUID'
@@ -1607,7 +1607,7 @@ paths:
1607 - admin 1607 - admin
1608 - moderator 1608 - moderator
1609 tags: 1609 tags:
1610 - Video Abuses 1610 - Abuses
1611 parameters: 1611 parameters:
1612 - $ref: '#/components/parameters/idOrUUID' 1612 - $ref: '#/components/parameters/idOrUUID'
1613 - $ref: '#/components/parameters/abuseId' 1613 - $ref: '#/components/parameters/abuseId'
@@ -1626,11 +1626,11 @@ paths:
1626 '204': 1626 '204':
1627 description: successful operation 1627 description: successful operation
1628 '404': 1628 '404':
1629 description: video abuse not found 1629 description: abuse not found
1630 delete: 1630 delete:
1631 deprecated: true 1631 deprecated: true
1632 tags: 1632 tags:
1633 - Video Abuses 1633 - Abuses
1634 summary: Delete an abuse 1634 summary: Delete an abuse
1635 security: 1635 security:
1636 - OAuth2: 1636 - OAuth2:
@@ -3320,7 +3320,7 @@ components:
3320 name: abuseId 3320 name: abuseId
3321 in: path 3321 in: path
3322 required: true 3322 required: true
3323 description: Video abuse id 3323 description: Abuse id
3324 schema: 3324 schema:
3325 type: integer 3325 type: integer
3326 captionLanguage: 3326 captionLanguage:
@@ -5098,7 +5098,7 @@ components:
5098 5098
5099 - `2` NEW_COMMENT_ON_MY_VIDEO 5099 - `2` NEW_COMMENT_ON_MY_VIDEO
5100 5100
5101 - `3` NEW_VIDEO_ABUSE_FOR_MODERATORS 5101 - `3` NEW_ABUSE_FOR_MODERATORS
5102 5102
5103 - `4` BLACKLIST_ON_MY_VIDEO 5103 - `4` BLACKLIST_ON_MY_VIDEO
5104 5104