aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-07-01 16:05:30 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-07-10 14:02:41 +0200
commitd95d15598847c7f020aa056e7e6e0c02d2bbf732 (patch)
treea8a593f1269688caf9e5f99559996f346290fec5 /server/lib
parent72493e44e9b455a04c4f093ed6c6ffa300b98d8b (diff)
downloadPeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.gz
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.tar.zst
PeerTube-d95d15598847c7f020aa056e7e6e0c02d2bbf732.zip
Use 3 tables to represent abuses
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/process/process-flag.ts117
-rw-r--r--server/lib/activitypub/send/send-flag.ts31
-rw-r--r--server/lib/activitypub/url.ts10
-rw-r--r--server/lib/emailer.ts110
-rw-r--r--server/lib/emails/account-abuse-new/html.pug14
-rw-r--r--server/lib/emails/common/mixins.pug6
-rw-r--r--server/lib/emails/video-abuse-new/html.pug8
-rw-r--r--server/lib/emails/video-comment-abuse-new/html.pug15
-rw-r--r--server/lib/moderation.ts164
-rw-r--r--server/lib/notifier.ts43
10 files changed, 374 insertions, 144 deletions
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 1d7132a3a..6350cee12 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -1,24 +1,19 @@
1import { 1import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
2 ActivityCreate, 2import { AccountModel } from '@server/models/account/account'
3 ActivityFlag, 3import { VideoModel } from '@server/models/video/video'
4 VideoAbuseState, 4import { VideoCommentModel } from '@server/models/video/video-comment'
5 videoAbusePredefinedReasonsMap 5import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
6} from '../../../../shared' 6import { getAPId } from '../../../helpers/activitypub'
7import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
8import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { sequelizeTypescript } from '../../../initializers/database' 9import { sequelizeTypescript } from '../../../initializers/database'
11import { VideoAbuseModel } from '../../../models/video/video-abuse'
12import { getOrCreateVideoAndAccountAndChannel } from '../videos'
13import { Notifier } from '../../notifier'
14import { getAPId } from '../../../helpers/activitypub'
15import { APProcessorOptions } from '../../../types/activitypub-processor.model' 10import { APProcessorOptions } from '../../../types/activitypub-processor.model'
16import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models' 11import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
17import { AccountModel } from '@server/models/account/account'
18 12
19async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
20 const { activity, byActor } = options 14 const { activity, byActor } = options
21 return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) 15
16 return retryTransactionWrapper(processCreateAbuse, activity, byActor)
22} 17}
23 18
24// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
@@ -29,55 +24,79 @@ export {
29 24
30// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
31 26
32async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { 27async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
33 const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) 28 const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
34 29
35 const account = byActor.Account 30 const account = byActor.Account
36 if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url) 31 if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
32
33 const reporterAccount = await AccountModel.load(account.id)
37 34
38 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] 35 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
39 36
37 const tags = Array.isArray(flag.tag) ? flag.tag : []
38 const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name])
39 .filter(v => !isNaN(v))
40
41 const startAt = flag.startAt
42 const endAt = flag.endAt
43
40 for (const object of objects) { 44 for (const object of objects) {
41 try { 45 try {
42 logger.debug('Reporting remote abuse for video %s.', getAPId(object)) 46 const uri = getAPId(object)
43
44 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
45 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
46 const tags = Array.isArray(flag.tag) ? flag.tag : []
47 const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
48 .filter(v => !isNaN(v))
49 const startAt = flag.startAt
50 const endAt = flag.endAt
51
52 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
53 const videoAbuseData = {
54 reporterAccountId: account.id,
55 reason: flag.content,
56 videoId: video.id,
57 state: VideoAbuseState.PENDING,
58 predefinedReasons,
59 startAt,
60 endAt
61 }
62 47
63 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) 48 logger.debug('Reporting remote abuse for object %s.', uri)
64 videoAbuseInstance.Video = video
65 videoAbuseInstance.Account = reporterAccount
66 49
67 logger.info('Remote abuse for video uuid %s created', flag.object) 50 await sequelizeTypescript.transaction(async t => {
68 51
69 return videoAbuseInstance 52 const video = await VideoModel.loadByUrlAndPopulateAccount(uri)
70 }) 53 let videoComment: MCommentOwnerVideo
54 let flaggedAccount: MAccountDefault
55
56 if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri)
57 if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri)
58
59 if (!video && !videoComment && !flaggedAccount) {
60 logger.warn('Cannot flag unknown entity %s.', object)
61 return
62 }
63
64 const baseAbuse = {
65 reporterAccountId: reporterAccount.id,
66 reason: flag.content,
67 state: AbuseState.PENDING,
68 predefinedReasons
69 }
71 70
72 const videoAbuseJSON = videoAbuseInstance.toFormattedJSON() 71 if (video) {
72 return createVideoAbuse({
73 baseAbuse,
74 startAt,
75 endAt,
76 reporterAccount,
77 transaction: t,
78 videoInstance: video
79 })
80 }
81
82 if (videoComment) {
83 return createVideoCommentAbuse({
84 baseAbuse,
85 reporterAccount,
86 transaction: t,
87 commentInstance: videoComment
88 })
89 }
73 90
74 Notifier.Instance.notifyOnNewVideoAbuse({ 91 return await createAccountAbuse({
75 videoAbuse: videoAbuseJSON, 92 baseAbuse,
76 videoAbuseInstance, 93 reporterAccount,
77 reporter: reporterAccount.Actor.getIdentifier() 94 transaction: t,
95 accountInstance: flaggedAccount
96 })
78 }) 97 })
79 } catch (err) { 98 } catch (err) {
80 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) 99 logger.debug('Cannot process report of %s', getAPId(object), { err })
81 } 100 }
82 } 101 }
83} 102}
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index 3a1fe0812..821637ec8 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -1,32 +1,31 @@
1import { getVideoAbuseActivityPubUrl } from '../url' 1import { Transaction } from 'sequelize'
2import { unicastTo } from './utils'
3import { logger } from '../../../helpers/logger'
4import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
3import { logger } from '../../../helpers/logger'
4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
5import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
6import { Transaction } from 'sequelize' 6import { getAbuseActivityPubUrl } from '../url'
7import { MActor, MVideoFullLight } from '../../../types/models' 7import { unicastTo } from './utils'
8import { MVideoAbuseVideo } from '../../../types/models/video'
9 8
10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 9function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 10 if (!flaggedAccount.Actor.serverId) return // Local user
12 11
13 const url = getVideoAbuseActivityPubUrl(videoAbuse) 12 const url = getAbuseActivityPubUrl(abuse)
14 13
15 logger.info('Creating job to send video abuse %s.', url) 14 logger.info('Creating job to send abuse %s.', url)
16 15
17 // Custom audience, we only send the abuse to the origin instance 16 // Custom audience, we only send the abuse to the origin instance
18 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } 17 const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
19 const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) 18 const flagActivity = buildFlagActivity(url, byActor, abuse, audience)
20 19
21 t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox())) 20 t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox()))
22} 21}
23 22
24function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag { 23function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag {
25 if (!audience) audience = getAudience(byActor) 24 if (!audience) audience = getAudience(byActor)
26 25
27 const activity = Object.assign( 26 const activity = Object.assign(
28 { id: url, actor: byActor.url }, 27 { id: url, actor: byActor.url },
29 videoAbuse.toActivityPubObject() 28 abuse.toActivityPubObject()
30 ) 29 )
31 30
32 return audiencify(activity, audience) 31 return audiencify(activity, audience)
@@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus
35// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
36 35
37export { 36export {
38 sendVideoAbuse 37 sendAbuse
39} 38}
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index 7f98751a1..b54e038a4 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -5,10 +5,10 @@ import {
5 MActorId, 5 MActorId,
6 MActorUrl, 6 MActorUrl,
7 MCommentId, 7 MCommentId,
8 MVideoAbuseId,
9 MVideoId, 8 MVideoId,
10 MVideoUrl, 9 MVideoUrl,
11 MVideoUUID 10 MVideoUUID,
11 MAbuseId
12} from '../../types/models' 12} from '../../types/models'
13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist' 13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
14import { MVideoFileVideoUUID } from '../../types/models/video/video-file' 14import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
@@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) {
48 return WEBSERVER.URL + '/accounts/' + accountName 48 return WEBSERVER.URL + '/accounts/' + accountName
49} 49}
50 50
51function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) { 51function getAbuseActivityPubUrl (abuse: MAbuseId) {
52 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id 52 return WEBSERVER.URL + '/admin/abuses/' + abuse.id
53} 53}
54 54
55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { 55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
@@ -118,7 +118,7 @@ export {
118 getVideoCacheStreamingPlaylistActivityPubUrl, 118 getVideoCacheStreamingPlaylistActivityPubUrl,
119 getVideoChannelActivityPubUrl, 119 getVideoChannelActivityPubUrl,
120 getAccountActivityPubUrl, 120 getAccountActivityPubUrl,
121 getVideoAbuseActivityPubUrl, 121 getAbuseActivityPubUrl,
122 getActorFollowActivityPubUrl, 122 getActorFollowActivityPubUrl,
123 getActorFollowAcceptActivityPubUrl, 123 getActorFollowAcceptActivityPubUrl,
124 getVideoAnnounceActivityPubUrl, 124 getVideoAnnounceActivityPubUrl,
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index c08732b48..e821aea5f 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,26 +1,20 @@
1import { readFileSync } from 'fs-extra'
2import { merge } from 'lodash'
1import { createTransport, Transporter } from 'nodemailer' 3import { createTransport, Transporter } from 'nodemailer'
4import { join } from 'path'
5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { Abuse, EmailPayload } from '@shared/models'
9import { SendEmailOptions } from '../../shared/models/server/emailer.model'
2import { isTestInstance, root } from '../helpers/core-utils' 10import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue'
6import { readFileSync } from 'fs-extra'
7import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
8import { 14import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
9 MCommentOwnerVideo, 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
10 MVideo, 16import { JobQueue } from './job-queue'
11 MVideoAbuseVideo, 17
12 MVideoAccountLight,
13 MVideoBlacklistLightVideo,
14 MVideoBlacklistVideo
15} from '../types/models/video'
16import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
17import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
18import { EmailPayload } from '@shared/models'
19import { join } from 'path'
20import { VideoAbuse } from '../../shared/models/videos'
21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
22import { merge } from 'lodash'
23import { VideoChannelModel } from '@server/models/video/video-channel'
24const Email = require('email-templates') 18const Email = require('email-templates')
25 19
26class Emailer { 20class Emailer {
@@ -288,28 +282,70 @@ class Emailer {
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 282 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 } 283 }
290 284
291 addVideoAbuseModeratorsNotification (to: string[], parameters: { 285 addAbuseModeratorsNotification (to: string[], parameters: {
292 videoAbuse: VideoAbuse 286 abuse: Abuse
293 videoAbuseInstance: MVideoAbuseVideo 287 abuseInstance: MAbuseFull
294 reporter: string 288 reporter: string
295 }) { 289 }) {
296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id 290 const { abuse, abuseInstance, reporter } = parameters
297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
298 291
299 const emailPayload: EmailPayload = { 292 const action = {
300 template: 'video-abuse-new', 293 text: 'View report #' + abuse.id,
301 to, 294 url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
302 subject: `New video abuse report from ${parameters.reporter}`, 295 }
303 locals: { 296
304 videoUrl, 297 let emailPayload: EmailPayload
305 videoAbuseUrl, 298
306 videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(), 299 if (abuseInstance.VideoAbuse) {
307 videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(), 300 const video = abuseInstance.VideoAbuse.Video
308 videoAbuse: parameters.videoAbuse, 301 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
309 reporter: parameters.reporter, 302
310 action: { 303 emailPayload = {
311 text: 'View report #' + parameters.videoAbuse.id, 304 template: 'video-abuse-new',
312 url: videoAbuseUrl 305 to,
306 subject: `New video abuse report from ${reporter}`,
307 locals: {
308 videoUrl,
309 isLocal: video.remote === false,
310 videoCreatedAt: new Date(video.createdAt).toLocaleString(),
311 videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
312 videoName: video.name,
313 reason: abuse.reason,
314 videoChannel: video.VideoChannel,
315 action
316 }
317 }
318 } else if (abuseInstance.VideoCommentAbuse) {
319 const comment = abuseInstance.VideoCommentAbuse.VideoComment
320 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
321
322 emailPayload = {
323 template: 'comment-abuse-new',
324 to,
325 subject: `New comment abuse report from ${reporter}`,
326 locals: {
327 commentUrl,
328 isLocal: comment.isOwned(),
329 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
330 reason: abuse.reason,
331 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
332 action
333 }
334 }
335 } else {
336 const account = abuseInstance.FlaggedAccount
337 const accountUrl = account.getClientUrl()
338
339 emailPayload = {
340 template: 'account-abuse-new',
341 to,
342 subject: `New account abuse report from ${reporter}`,
343 locals: {
344 accountUrl,
345 accountDisplayName: account.getDisplayName(),
346 isLocal: account.isOwned(),
347 reason: abuse.reason,
348 action
313 } 349 }
314 } 350 }
315 } 351 }
diff --git a/server/lib/emails/account-abuse-new/html.pug b/server/lib/emails/account-abuse-new/html.pug
new file mode 100644
index 000000000..06be8025b
--- /dev/null
+++ b/server/lib/emails/account-abuse-new/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | An account is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account "
10 a(href=accountUrl) #{accountDisplayName}
11
12 p The reporter, #{reporter}, cited the following reason(s):
13 blockquote #{reason}
14 br(style="display: none;")
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug
index 76b805a24..831211864 100644
--- a/server/lib/emails/common/mixins.pug
+++ b/server/lib/emails/common/mixins.pug
@@ -1,3 +1,7 @@
1mixin channel(channel) 1mixin channel(channel)
2 - var handle = `${channel.name}@${channel.host}` 2 - var handle = `${channel.name}@${channel.host}`
3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file 3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
4
5mixin account(account)
6 - var handle = `${account.name}@${account.host}`
7 | #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug
index 999c89d26..a1acdabdc 100644
--- a/server/lib/emails/video-abuse-new/html.pug
+++ b/server/lib/emails/video-abuse-new/html.pug
@@ -6,13 +6,13 @@ block title
6 6
7block content 7block content
8 p 8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video " 9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
10 a(href=videoUrl) #{videoAbuse.video.name} 10 a(href=videoUrl) #{videoName}
11 | " by #[+channel(videoAbuse.video.channel)] 11 | " by #[+channel(videoChannel)]
12 if videoPublishedAt 12 if videoPublishedAt
13 | , published the #{videoPublishedAt}. 13 | , published the #{videoPublishedAt}.
14 else 14 else
15 | , uploaded the #{videoCreatedAt} but not yet published. 15 | , uploaded the #{videoCreatedAt} but not yet published.
16 p The reporter, #{reporter}, cited the following reason(s): 16 p The reporter, #{reporter}, cited the following reason(s):
17 blockquote #{videoAbuse.reason} 17 blockquote #{reason}
18 br(style="display: none;") 18 br(style="display: none;")
diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug
new file mode 100644
index 000000000..170b79576
--- /dev/null
+++ b/server/lib/emails/video-comment-abuse-new/html.pug
@@ -0,0 +1,15 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A comment is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment "
10 a(href=commentUrl) of #{flaggedAccount}
11 | created on #{commentCreatedAt}
12
13 p The reporter, #{reporter}, cited the following reason(s):
14 blockquote #{reason}
15 br(style="display: none;")
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index 60d1b4053..4fc9cd747 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -1,15 +1,33 @@
1import { VideoModel } from '../models/video/video' 1import { PathLike } from 'fs-extra'
2import { VideoCommentModel } from '../models/video/video-comment' 2import { Transaction } from 'sequelize/types'
3import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' 3import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
4import { logger } from '@server/helpers/logger'
5import { AbuseModel } from '@server/models/abuse/abuse'
6import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
7import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { FilteredModelAttributes } from '@server/types'
10import {
11 MAbuseFull,
12 MAccountDefault,
13 MAccountLight,
14 MCommentAbuseAccountVideo,
15 MCommentOwnerVideo,
16 MUser,
17 MVideoAbuseVideoFull,
18 MVideoAccountLightBlacklistAllFiles
19} from '@server/types/models'
20import { ActivityCreate } from '../../shared/models/activitypub'
21import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
22import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
4import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' 23import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
24import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
5import { UserModel } from '../models/account/user' 25import { UserModel } from '../models/account/user'
6import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
7import { ActivityCreate } from '../../shared/models/activitypub'
8import { ActorModel } from '../models/activitypub/actor' 26import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' 27import { VideoModel } from '../models/video/video'
10import { VideoFileModel } from '@server/models/video/video-file' 28import { VideoCommentModel } from '../models/video/video-comment'
11import { PathLike } from 'fs-extra' 29import { sendAbuse } from './activitypub/send/send-flag'
12import { MUser } from '@server/types/models' 30import { Notifier } from './notifier'
13 31
14export type AcceptResult = { 32export type AcceptResult = {
15 accepted: boolean 33 accepted: boolean
@@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: {
73 return { accepted: true } 91 return { accepted: true }
74} 92}
75 93
94async function createVideoAbuse (options: {
95 baseAbuse: FilteredModelAttributes<AbuseModel>
96 videoInstance: MVideoAccountLightBlacklistAllFiles
97 startAt: number
98 endAt: number
99 transaction: Transaction
100 reporterAccount: MAccountDefault
101}) {
102 const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options
103
104 const associateFun = async (abuseInstance: MAbuseFull) => {
105 const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
106 abuseId: abuseInstance.id,
107 videoId: videoInstance.id,
108 startAt: startAt,
109 endAt: endAt
110 }, { transaction })
111
112 videoAbuseInstance.Video = videoInstance
113 abuseInstance.VideoAbuse = videoAbuseInstance
114
115 return { isOwned: videoInstance.isOwned() }
116 }
117
118 return createAbuse({
119 base: baseAbuse,
120 reporterAccount,
121 flaggedAccount: videoInstance.VideoChannel.Account,
122 transaction,
123 associateFun
124 })
125}
126
127function createVideoCommentAbuse (options: {
128 baseAbuse: FilteredModelAttributes<AbuseModel>
129 commentInstance: MCommentOwnerVideo
130 transaction: Transaction
131 reporterAccount: MAccountDefault
132}) {
133 const { baseAbuse, commentInstance, transaction, reporterAccount } = options
134
135 const associateFun = async (abuseInstance: MAbuseFull) => {
136 const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
137 abuseId: abuseInstance.id,
138 videoCommentId: commentInstance.id
139 }, { transaction })
140
141 commentAbuseInstance.VideoComment = commentInstance
142 abuseInstance.VideoCommentAbuse = commentAbuseInstance
143
144 return { isOwned: commentInstance.isOwned() }
145 }
146
147 return createAbuse({
148 base: baseAbuse,
149 reporterAccount,
150 flaggedAccount: commentInstance.Account,
151 transaction,
152 associateFun
153 })
154}
155
156function createAccountAbuse (options: {
157 baseAbuse: FilteredModelAttributes<AbuseModel>
158 accountInstance: MAccountDefault
159 transaction: Transaction
160 reporterAccount: MAccountDefault
161}) {
162 const { baseAbuse, accountInstance, transaction, reporterAccount } = options
163
164 const associateFun = async () => {
165 return { isOwned: accountInstance.isOwned() }
166 }
167
168 return createAbuse({
169 base: baseAbuse,
170 reporterAccount,
171 flaggedAccount: accountInstance,
172 transaction,
173 associateFun
174 })
175}
176
76export { 177export {
77 isLocalVideoAccepted, 178 isLocalVideoAccepted,
78 isLocalVideoThreadAccepted, 179 isLocalVideoThreadAccepted,
@@ -80,5 +181,48 @@ export {
80 isRemoteVideoCommentAccepted, 181 isRemoteVideoCommentAccepted,
81 isLocalVideoCommentReplyAccepted, 182 isLocalVideoCommentReplyAccepted,
82 isPreImportVideoAccepted, 183 isPreImportVideoAccepted,
83 isPostImportVideoAccepted 184 isPostImportVideoAccepted,
185
186 createAbuse,
187 createVideoAbuse,
188 createVideoCommentAbuse,
189 createAccountAbuse
190}
191
192// ---------------------------------------------------------------------------
193
194async function createAbuse (options: {
195 base: FilteredModelAttributes<AbuseModel>
196 reporterAccount: MAccountDefault
197 flaggedAccount: MAccountLight
198 associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} >
199 transaction: Transaction
200}) {
201 const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options
202 const auditLogger = auditLoggerFactory('abuse')
203
204 const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
205 const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction })
206
207 abuseInstance.ReporterAccount = reporterAccount
208 abuseInstance.FlaggedAccount = flaggedAccount
209
210 const { isOwned } = await associateFun(abuseInstance)
211
212 if (isOwned === false) {
213 await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
214 }
215
216 const abuseJSON = abuseInstance.toFormattedJSON()
217 auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
218
219 Notifier.Instance.notifyOnNewAbuse({
220 abuse: abuseJSON,
221 abuseInstance,
222 reporter: reporterAccount.Actor.getIdentifier()
223 })
224
225 logger.info('Abuse report %d created.', abuseInstance.id)
226
227 return abuseJSON
84} 228}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 943a087d2..40cff66d2 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -8,23 +8,18 @@ import {
8 MUserWithNotificationSetting, 8 MUserWithNotificationSetting,
9 UserNotificationModelForApi 9 UserNotificationModelForApi
10} from '@server/types/models/user' 10} from '@server/types/models/user'
11import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
11import { MVideoImportVideo } from '@server/types/models/video/video-import' 12import { MVideoImportVideo } from '@server/types/models/video/video-import'
13import { Abuse } from '@shared/models'
12import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' 14import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
13import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos' 15import { VideoPrivacy, VideoState } from '../../shared/models/videos'
14import { logger } from '../helpers/logger' 16import { logger } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 17import { CONFIG } from '../initializers/config'
16import { AccountBlocklistModel } from '../models/account/account-blocklist' 18import { AccountBlocklistModel } from '../models/account/account-blocklist'
17import { UserModel } from '../models/account/user' 19import { UserModel } from '../models/account/user'
18import { UserNotificationModel } from '../models/account/user-notification' 20import { UserNotificationModel } from '../models/account/user-notification'
19import { MAccountServer, MActorFollowFull } from '../types/models' 21import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
20import { 22import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
21 MCommentOwnerVideo,
22 MVideoAbuseVideo,
23 MVideoAccountLight,
24 MVideoBlacklistLightVideo,
25 MVideoBlacklistVideo,
26 MVideoFullLight
27} from '../types/models/video'
28import { isBlockedByServerOrAccount } from './blocklist' 23import { isBlockedByServerOrAccount } from './blocklist'
29import { Emailer } from './emailer' 24import { Emailer } from './emailer'
30import { PeerTubeSocket } from './peertube-socket' 25import { PeerTubeSocket } from './peertube-socket'
@@ -78,9 +73,9 @@ class Notifier {
78 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 73 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
79 } 74 }
80 75
81 notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void { 76 notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
82 this.notifyModeratorsOfNewVideoAbuse(parameters) 77 this.notifyModeratorsOfNewAbuse(parameters)
83 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err })) 78 .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
84 } 79 }
85 80
86 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { 81 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
@@ -354,33 +349,37 @@ class Notifier {
354 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 349 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
355 } 350 }
356 351
357 private async notifyModeratorsOfNewVideoAbuse (parameters: { 352 private async notifyModeratorsOfNewAbuse (parameters: {
358 videoAbuse: VideoAbuse 353 abuse: Abuse
359 videoAbuseInstance: MVideoAbuseVideo 354 abuseInstance: MAbuseFull
360 reporter: string 355 reporter: string
361 }) { 356 }) {
362 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 357 const { abuse, abuseInstance } = parameters
358
359 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
363 if (moderators.length === 0) return 360 if (moderators.length === 0) return
364 361
365 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url) 362 const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url
363
364 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
366 365
367 function settingGetter (user: MUserWithNotificationSetting) { 366 function settingGetter (user: MUserWithNotificationSetting) {
368 return user.NotificationSetting.videoAbuseAsModerator 367 return user.NotificationSetting.videoAbuseAsModerator
369 } 368 }
370 369
371 async function notificationCreator (user: MUserWithNotificationSetting) { 370 async function notificationCreator (user: MUserWithNotificationSetting) {
372 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 371 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
373 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 372 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
374 userId: user.id, 373 userId: user.id,
375 videoAbuseId: parameters.videoAbuse.id 374 abuseId: abuse.id
376 }) 375 })
377 notification.VideoAbuse = parameters.videoAbuseInstance 376 notification.Abuse = abuseInstance
378 377
379 return notification 378 return notification
380 } 379 }
381 380
382 function emailSender (emails: string[]) { 381 function emailSender (emails: string[]) {
383 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) 382 return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
384 } 383 }
385 384
386 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 385 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })