aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/abuse/abuse-query-builder.ts154
-rw-r--r--server/models/abuse/abuse.ts515
-rw-r--r--server/models/abuse/video-abuse.ts63
-rw-r--r--server/models/abuse/video-comment-abuse.ts47
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts9
-rw-r--r--server/models/account/user-notification-setting.ts8
-rw-r--r--server/models/account/user-notification.ts102
-rw-r--r--server/models/account/user.ts38
-rw-r--r--server/models/server/server-blocklist.ts10
-rw-r--r--server/models/video/video-abuse.ts479
-rw-r--r--server/models/video/video-channel.ts3
-rw-r--r--server/models/video/video-comment.ts27
-rw-r--r--server/models/video/video-query-builder.ts4
-rw-r--r--server/models/video/video.ts88
15 files changed, 969 insertions, 588 deletions
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts
new file mode 100644
index 000000000..5fddcf3c4
--- /dev/null
+++ b/server/models/abuse/abuse-query-builder.ts
@@ -0,0 +1,154 @@
1
2import { exists } from '@server/helpers/custom-validators/misc'
3import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models'
4import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils'
5
6export type BuildAbusesQueryOptions = {
7 start: number
8 count: number
9 sort: string
10
11 // search
12 search?: string
13 searchReporter?: string
14 searchReportee?: string
15
16 // video releated
17 searchVideo?: string
18 searchVideoChannel?: string
19 videoIs?: AbuseVideoIs
20
21 // filters
22 id?: number
23 predefinedReasonId?: number
24 filter?: AbuseFilter
25
26 state?: AbuseState
27
28 // accountIds
29 serverAccountId: number
30 userAccountId: number
31}
32
33function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') {
34 const whereAnd: string[] = []
35 const replacements: any = {}
36
37 const joins = [
38 'LEFT JOIN "videoAbuse" ON "videoAbuse"."abuseId" = "abuse"."id"',
39 'LEFT JOIN "video" ON "videoAbuse"."videoId" = "video"."id"',
40 'LEFT JOIN "videoBlacklist" ON "videoBlacklist"."videoId" = "video"."id"',
41 'LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"',
42 'LEFT JOIN "account" "reporterAccount" ON "reporterAccount"."id" = "abuse"."reporterAccountId"',
43 'LEFT JOIN "account" "flaggedAccount" ON "flaggedAccount"."id" = "abuse"."reporterAccountId"',
44 'LEFT JOIN "commentAbuse" ON "commentAbuse"."abuseId" = "abuse"."id"',
45 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"'
46 ]
47
48 whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
49
50 if (options.search) {
51 const searchWhereOr = [
52 '"video"."name" ILIKE :search',
53 '"videoChannel"."name" ILIKE :search',
54 `"videoAbuse"."deletedVideo"->>'name' ILIKE :search`,
55 `"videoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE :search`,
56 '"reporterAccount"."name" ILIKE :search',
57 '"flaggedAccount"."name" ILIKE :search'
58 ]
59
60 replacements.search = `%${options.search}%`
61 whereAnd.push('(' + searchWhereOr.join(' OR ') + ')')
62 }
63
64 if (options.searchVideo) {
65 whereAnd.push('"video"."name" ILIKE :searchVideo')
66 replacements.searchVideo = `%${options.searchVideo}%`
67 }
68
69 if (options.searchVideoChannel) {
70 whereAnd.push('"videoChannel"."name" ILIKE :searchVideoChannel')
71 replacements.searchVideoChannel = `%${options.searchVideoChannel}%`
72 }
73
74 if (options.id) {
75 whereAnd.push('"abuse"."id" = :id')
76 replacements.id = options.id
77 }
78
79 if (options.state) {
80 whereAnd.push('"abuse"."state" = :state')
81 replacements.state = options.state
82 }
83
84 if (options.videoIs === 'deleted') {
85 whereAnd.push('"videoAbuse"."deletedVideo" IS NOT NULL')
86 } else if (options.videoIs === 'blacklisted') {
87 whereAnd.push('"videoBlacklist"."id" IS NOT NULL')
88 }
89
90 if (options.predefinedReasonId) {
91 whereAnd.push(':predefinedReasonId = ANY("abuse"."predefinedReasons")')
92 replacements.predefinedReasonId = options.predefinedReasonId
93 }
94
95 if (options.filter === 'video') {
96 whereAnd.push('"videoAbuse"."id" IS NOT NULL')
97 } else if (options.filter === 'comment') {
98 whereAnd.push('"commentAbuse"."id" IS NOT NULL')
99 } else if (options.filter === 'account') {
100 whereAnd.push('"videoAbuse"."id" IS NULL AND "commentAbuse"."id" IS NULL')
101 }
102
103 if (options.searchReporter) {
104 whereAnd.push('"reporterAccount"."name" ILIKE :searchReporter')
105 replacements.searchReporter = `%${options.searchReporter}%`
106 }
107
108 if (options.searchReportee) {
109 whereAnd.push('"flaggedAccount"."name" ILIKE :searchReportee')
110 replacements.searchReportee = `%${options.searchReportee}%`
111 }
112
113 const prefix = type === 'count'
114 ? 'SELECT COUNT("abuse"."id") AS "total"'
115 : 'SELECT "abuse"."id" '
116
117 let suffix = ''
118 if (type !== 'count') {
119
120 if (options.sort) {
121 const order = buildAbuseOrder(options.sort)
122 suffix += `${order} `
123 }
124
125 if (exists(options.count)) {
126 const count = parseInt(options.count + '', 10)
127 suffix += `LIMIT ${count} `
128 }
129
130 if (exists(options.start)) {
131 const start = parseInt(options.start + '', 10)
132 suffix += `OFFSET ${start} `
133 }
134 }
135
136 const where = whereAnd.length !== 0
137 ? `WHERE ${whereAnd.join(' AND ')}`
138 : ''
139
140 return {
141 query: `${prefix} FROM "abuse" ${joins.join(' ')} ${where} ${suffix}`,
142 replacements
143 }
144}
145
146function buildAbuseOrder (value: string) {
147 const { direction, field } = buildDirectionAndField(value)
148
149 return `ORDER BY "abuse"."${field}" ${direction}`
150}
151
152export {
153 buildAbuseListQuery
154}
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
new file mode 100644
index 000000000..bd96cf79c
--- /dev/null
+++ b/server/models/abuse/abuse.ts
@@ -0,0 +1,515 @@
1import * as Bluebird from 'bluebird'
2import { invert } from 'lodash'
3import { literal, Op, QueryTypes, WhereOptions } from 'sequelize'
4import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasOne,
13 Is,
14 Model,
15 Scopes,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
19import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
20import {
21 Abuse,
22 AbuseFilter,
23 AbuseObject,
24 AbusePredefinedReasons,
25 abusePredefinedReasonsMap,
26 AbusePredefinedReasonsString,
27 AbuseState,
28 AbuseVideoIs,
29 VideoAbuse,
30 VideoCommentAbuse
31} from '@shared/models'
32import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants'
33import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
34import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
35import { getSort, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from '../video/thumbnail'
37import { VideoModel } from '../video/video'
38import { VideoBlacklistModel } from '../video/video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel'
40import { VideoCommentModel } from '../video/video-comment'
41import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder'
42import { VideoAbuseModel } from './video-abuse'
43import { VideoCommentAbuseModel } from './video-comment-abuse'
44
45export enum ScopeNames {
46 FOR_API = 'FOR_API'
47}
48
49@Scopes(() => ({
50 [ScopeNames.FOR_API]: () => {
51 return {
52 attributes: {
53 include: [
54 [
55 // we don't care about this count for deleted videos, so there are not included
56 literal(
57 '(' +
58 'SELECT count(*) ' +
59 'FROM "videoAbuse" ' +
60 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' +
61 ')'
62 ),
63 'countReportsForVideo'
64 ],
65 [
66 // we don't care about this count for deleted videos, so there are not included
67 literal(
68 '(' +
69 'SELECT t.nth ' +
70 'FROM ( ' +
71 'SELECT id, ' +
72 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
73 'FROM "videoAbuse" ' +
74 ') t ' +
75 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' +
76 ')'
77 ),
78 'nthReportForVideo'
79 ],
80 [
81 literal(
82 '(' +
83 'SELECT count("abuse"."id") ' +
84 'FROM "abuse" ' +
85 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' +
86 ')'
87 ),
88 'countReportsForReporter'
89 ],
90 [
91 literal(
92 '(' +
93 'SELECT count("abuse"."id") ' +
94 'FROM "abuse" ' +
95 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' +
96 ')'
97 ),
98 'countReportsForReportee'
99 ]
100 ]
101 },
102 include: [
103 {
104 model: AccountModel.scope({
105 method: [
106 AccountScopeNames.SUMMARY,
107 { actorRequired: false } as AccountSummaryOptions
108 ]
109 }),
110 as: 'ReporterAccount'
111 },
112 {
113 model: AccountModel.scope({
114 method: [
115 AccountScopeNames.SUMMARY,
116 { actorRequired: false } as AccountSummaryOptions
117 ]
118 }),
119 as: 'FlaggedAccount'
120 },
121 {
122 model: VideoCommentAbuseModel.unscoped(),
123 include: [
124 {
125 model: VideoCommentModel.unscoped(),
126 include: [
127 {
128 model: VideoModel.unscoped(),
129 attributes: [ 'name', 'id', 'uuid' ]
130 }
131 ]
132 }
133 ]
134 },
135 {
136 model: VideoAbuseModel.unscoped(),
137 include: [
138 {
139 attributes: [ 'id', 'uuid', 'name', 'nsfw' ],
140 model: VideoModel.unscoped(),
141 include: [
142 {
143 attributes: [ 'filename', 'fileUrl', 'type' ],
144 model: ThumbnailModel
145 },
146 {
147 model: VideoChannelModel.scope({
148 method: [
149 VideoChannelScopeNames.SUMMARY,
150 { withAccount: false, actorRequired: false } as ChannelSummaryOptions
151 ]
152 }),
153 required: false
154 },
155 {
156 attributes: [ 'id', 'reason', 'unfederated' ],
157 required: false,
158 model: VideoBlacklistModel
159 }
160 ]
161 }
162 ]
163 }
164 ]
165 }
166 }
167}))
168@Table({
169 tableName: 'abuse',
170 indexes: [
171 {
172 fields: [ 'reporterAccountId' ]
173 },
174 {
175 fields: [ 'flaggedAccountId' ]
176 }
177 ]
178})
179export class AbuseModel extends Model<AbuseModel> {
180
181 @AllowNull(false)
182 @Default(null)
183 @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
184 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
185 reason: string
186
187 @AllowNull(false)
188 @Default(null)
189 @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
190 @Column
191 state: AbuseState
192
193 @AllowNull(true)
194 @Default(null)
195 @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
196 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
197 moderationComment: string
198
199 @AllowNull(true)
200 @Default(null)
201 @Column(DataType.ARRAY(DataType.INTEGER))
202 predefinedReasons: AbusePredefinedReasons[]
203
204 @CreatedAt
205 createdAt: Date
206
207 @UpdatedAt
208 updatedAt: Date
209
210 @ForeignKey(() => AccountModel)
211 @Column
212 reporterAccountId: number
213
214 @BelongsTo(() => AccountModel, {
215 foreignKey: {
216 name: 'reporterAccountId',
217 allowNull: true
218 },
219 as: 'ReporterAccount',
220 onDelete: 'set null'
221 })
222 ReporterAccount: AccountModel
223
224 @ForeignKey(() => AccountModel)
225 @Column
226 flaggedAccountId: number
227
228 @BelongsTo(() => AccountModel, {
229 foreignKey: {
230 name: 'flaggedAccountId',
231 allowNull: true
232 },
233 as: 'FlaggedAccount',
234 onDelete: 'set null'
235 })
236 FlaggedAccount: AccountModel
237
238 @HasOne(() => VideoCommentAbuseModel, {
239 foreignKey: {
240 name: 'abuseId',
241 allowNull: false
242 },
243 onDelete: 'cascade'
244 })
245 VideoCommentAbuse: VideoCommentAbuseModel
246
247 @HasOne(() => VideoAbuseModel, {
248 foreignKey: {
249 name: 'abuseId',
250 allowNull: false
251 },
252 onDelete: 'cascade'
253 })
254 VideoAbuse: VideoAbuseModel
255
256 // FIXME: deprecated in 2.3. Remove these validators
257 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
258 const videoWhere: WhereOptions = {}
259
260 if (videoId) videoWhere.videoId = videoId
261 if (uuid) videoWhere.deletedVideo = { uuid }
262
263 const query = {
264 include: [
265 {
266 model: VideoAbuseModel,
267 required: true,
268 where: videoWhere
269 }
270 ],
271 where: {
272 id
273 }
274 }
275 return AbuseModel.findOne(query)
276 }
277
278 static loadById (id: number): Bluebird<MAbuse> {
279 const query = {
280 where: {
281 id
282 }
283 }
284
285 return AbuseModel.findOne(query)
286 }
287
288 static async listForApi (parameters: {
289 start: number
290 count: number
291 sort: string
292
293 filter?: AbuseFilter
294
295 serverAccountId: number
296 user?: MUserAccountId
297
298 id?: number
299 predefinedReason?: AbusePredefinedReasonsString
300 state?: AbuseState
301 videoIs?: AbuseVideoIs
302
303 search?: string
304 searchReporter?: string
305 searchReportee?: string
306 searchVideo?: string
307 searchVideoChannel?: string
308 }) {
309 const {
310 start,
311 count,
312 sort,
313 search,
314 user,
315 serverAccountId,
316 state,
317 videoIs,
318 predefinedReason,
319 searchReportee,
320 searchVideo,
321 filter,
322 searchVideoChannel,
323 searchReporter,
324 id
325 } = parameters
326
327 const userAccountId = user ? user.Account.id : undefined
328 const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
329
330 const queryOptions: BuildAbusesQueryOptions = {
331 start,
332 count,
333 sort,
334 id,
335 filter,
336 predefinedReasonId,
337 search,
338 state,
339 videoIs,
340 searchReportee,
341 searchVideo,
342 searchVideoChannel,
343 searchReporter,
344 serverAccountId,
345 userAccountId
346 }
347
348 const [ total, data ] = await Promise.all([
349 AbuseModel.internalCountForApi(queryOptions),
350 AbuseModel.internalListForApi(queryOptions)
351 ])
352
353 return { total, data }
354 }
355
356 toFormattedJSON (this: MAbuseFormattable): Abuse {
357 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
358
359 const countReportsForVideo = this.get('countReportsForVideo') as number
360 const nthReportForVideo = this.get('nthReportForVideo') as number
361
362 const countReportsForReporter = this.get('countReportsForReporter') as number
363 const countReportsForReportee = this.get('countReportsForReportee') as number
364
365 let video: VideoAbuse = null
366 let comment: VideoCommentAbuse = null
367
368 if (this.VideoAbuse) {
369 const abuseModel = this.VideoAbuse
370 const entity = abuseModel.Video || abuseModel.deletedVideo
371
372 video = {
373 id: entity.id,
374 uuid: entity.uuid,
375 name: entity.name,
376 nsfw: entity.nsfw,
377
378 startAt: abuseModel.startAt,
379 endAt: abuseModel.endAt,
380
381 deleted: !abuseModel.Video,
382 blacklisted: abuseModel.Video?.isBlacklisted() || false,
383 thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
384
385 channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel,
386
387 countReports: countReportsForVideo,
388 nthReport: nthReportForVideo
389 }
390 }
391
392 if (this.VideoCommentAbuse) {
393 const abuseModel = this.VideoCommentAbuse
394 const entity = abuseModel.VideoComment
395
396 comment = {
397 id: entity.id,
398 threadId: entity.getThreadId(),
399
400 text: entity.text ?? '',
401
402 deleted: entity.isDeleted(),
403
404 video: {
405 id: entity.Video.id,
406 name: entity.Video.name,
407 uuid: entity.Video.uuid
408 }
409 }
410 }
411
412 return {
413 id: this.id,
414 reason: this.reason,
415 predefinedReasons,
416
417 reporterAccount: this.ReporterAccount
418 ? this.ReporterAccount.toFormattedJSON()
419 : null,
420
421 flaggedAccount: this.FlaggedAccount
422 ? this.FlaggedAccount.toFormattedJSON()
423 : null,
424
425 state: {
426 id: this.state,
427 label: AbuseModel.getStateLabel(this.state)
428 },
429
430 moderationComment: this.moderationComment,
431
432 video,
433 comment,
434
435 createdAt: this.createdAt,
436 updatedAt: this.updatedAt,
437
438 countReportsForReporter: (countReportsForReporter || 0),
439 countReportsForReportee: (countReportsForReportee || 0),
440
441 // FIXME: deprecated in 2.3, remove this
442 startAt: null,
443 endAt: null,
444 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0
446 }
447 }
448
449 toActivityPubObject (this: MAbuseAP): AbuseObject {
450 const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
451
452 const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
453
454 const startAt = this.VideoAbuse?.startAt
455 const endAt = this.VideoAbuse?.endAt
456
457 return {
458 type: 'Flag' as 'Flag',
459 content: this.reason,
460 object,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
467 }
468 }
469
470 private static async internalCountForApi (parameters: BuildAbusesQueryOptions) {
471 const { query, replacements } = buildAbuseListQuery(parameters, 'count')
472 const options = {
473 type: QueryTypes.SELECT as QueryTypes.SELECT,
474 replacements
475 }
476
477 const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options)
478 if (total === null) return 0
479
480 return parseInt(total, 10)
481 }
482
483 private static async internalListForApi (parameters: BuildAbusesQueryOptions) {
484 const { query, replacements } = buildAbuseListQuery(parameters, 'id')
485 const options = {
486 type: QueryTypes.SELECT as QueryTypes.SELECT,
487 replacements
488 }
489
490 const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options)
491 const ids = rows.map(r => r.id)
492
493 if (ids.length === 0) return []
494
495 return AbuseModel.scope(ScopeNames.FOR_API)
496 .findAll({
497 order: getSort(parameters.sort),
498 where: {
499 id: {
500 [Op.in]: ids
501 }
502 }
503 })
504 }
505
506 private static getStateLabel (id: number) {
507 return ABUSE_STATES[id] || 'Unknown'
508 }
509
510 private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
511 return (predefinedReasons || [])
512 .filter(r => r in AbusePredefinedReasons)
513 .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
514 }
515}
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts
new file mode 100644
index 000000000..d92bcf19f
--- /dev/null
+++ b/server/models/abuse/video-abuse.ts
@@ -0,0 +1,63 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoDetails } from '@shared/models'
3import { VideoModel } from '../video/video'
4import { AbuseModel } from './abuse'
5
6@Table({
7 tableName: 'videoAbuse',
8 indexes: [
9 {
10 fields: [ 'abuseId' ]
11 },
12 {
13 fields: [ 'videoId' ]
14 }
15 ]
16})
17export class VideoAbuseModel extends Model<VideoAbuseModel> {
18
19 @CreatedAt
20 createdAt: Date
21
22 @UpdatedAt
23 updatedAt: Date
24
25 @AllowNull(true)
26 @Default(null)
27 @Column
28 startAt: number
29
30 @AllowNull(true)
31 @Default(null)
32 @Column
33 endAt: number
34
35 @AllowNull(true)
36 @Default(null)
37 @Column(DataType.JSONB)
38 deletedVideo: VideoDetails
39
40 @ForeignKey(() => AbuseModel)
41 @Column
42 abuseId: number
43
44 @BelongsTo(() => AbuseModel, {
45 foreignKey: {
46 allowNull: false
47 },
48 onDelete: 'cascade'
49 })
50 Abuse: AbuseModel
51
52 @ForeignKey(() => VideoModel)
53 @Column
54 videoId: number
55
56 @BelongsTo(() => VideoModel, {
57 foreignKey: {
58 allowNull: true
59 },
60 onDelete: 'set null'
61 })
62 Video: VideoModel
63}
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts
new file mode 100644
index 000000000..8b34009b4
--- /dev/null
+++ b/server/models/abuse/video-comment-abuse.ts
@@ -0,0 +1,47 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoCommentModel } from '../video/video-comment'
3import { AbuseModel } from './abuse'
4
5@Table({
6 tableName: 'commentAbuse',
7 indexes: [
8 {
9 fields: [ 'abuseId' ]
10 },
11 {
12 fields: [ 'videoCommentId' ]
13 }
14 ]
15})
16export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
17
18 @CreatedAt
19 createdAt: Date
20
21 @UpdatedAt
22 updatedAt: Date
23
24 @ForeignKey(() => AbuseModel)
25 @Column
26 abuseId: number
27
28 @BelongsTo(() => AbuseModel, {
29 foreignKey: {
30 allowNull: false
31 },
32 onDelete: 'cascade'
33 })
34 Abuse: AbuseModel
35
36 @ForeignKey(() => VideoCommentModel)
37 @Column
38 videoCommentId: number
39
40 @BelongsTo(() => VideoCommentModel, {
41 foreignKey: {
42 allowNull: true
43 },
44 onDelete: 'set null'
45 })
46 VideoComment: VideoCommentModel
47}
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index cf8872fd5..577b7dc19 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -1,12 +1,12 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account'
3import { getSort, searchAttribute } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize'
6import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
7import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
5import { AccountBlock } from '../../../shared/models'
8import { ActorModel } from '../activitypub/actor' 6import { ActorModel } from '../activitypub/actor'
9import { ServerModel } from '../server/server' 7import { ServerModel } from '../server/server'
8import { getSort, searchAttribute } from '../utils'
9import { AccountModel } from './account'
10 10
11enum ScopeNames { 11enum ScopeNames {
12 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 12 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 4395d179a..f97519b14 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -42,6 +42,7 @@ export enum ScopeNames {
42} 42}
43 43
44export type SummaryOptions = { 44export type SummaryOptions = {
45 actorRequired?: boolean // Default: true
45 whereActor?: WhereOptions 46 whereActor?: WhereOptions
46 withAccountBlockerIds?: number[] 47 withAccountBlockerIds?: number[]
47} 48}
@@ -65,12 +66,12 @@ export type SummaryOptions = {
65 } 66 }
66 67
67 const query: FindOptions = { 68 const query: FindOptions = {
68 attributes: [ 'id', 'name' ], 69 attributes: [ 'id', 'name', 'actorId' ],
69 include: [ 70 include: [
70 { 71 {
71 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 72 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
72 model: ActorModel.unscoped(), 73 model: ActorModel.unscoped(),
73 required: true, 74 required: options.actorRequired ?? true,
74 where: whereActor, 75 where: whereActor,
75 include: [ 76 include: [
76 serverInclude, 77 serverInclude,
@@ -388,6 +389,10 @@ export class AccountModel extends Model<AccountModel> {
388 .findAll(query) 389 .findAll(query)
389 } 390 }
390 391
392 getClientUrl () {
393 return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
394 }
395
391 toFormattedJSON (this: MAccountFormattable): Account { 396 toFormattedJSON (this: MAccountFormattable): Account {
392 const actor = this.Actor.toFormattedJSON() 397 const actor = this.Actor.toFormattedJSON()
393 const account = { 398 const account = {
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index b69b47265..d8f3f13da 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
51 @AllowNull(false) 51 @AllowNull(false)
52 @Default(null) 52 @Default(null)
53 @Is( 53 @Is(
54 'UserNotificationSettingVideoAbuseAsModerator', 54 'UserNotificationSettingAbuseAsModerator',
55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') 55 value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
56 ) 56 )
57 @Column 57 @Column
58 videoAbuseAsModerator: UserNotificationSettingValue 58 abuseAsModerator: UserNotificationSettingValue
59 59
60 @AllowNull(false) 60 @AllowNull(false)
61 @Default(null) 61 @Default(null)
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
166 return { 166 return {
167 newCommentOnMyVideo: this.newCommentOnMyVideo, 167 newCommentOnMyVideo: this.newCommentOnMyVideo,
168 newVideoFromSubscription: this.newVideoFromSubscription, 168 newVideoFromSubscription: this.newVideoFromSubscription,
169 videoAbuseAsModerator: this.videoAbuseAsModerator, 169 abuseAsModerator: this.abuseAsModerator,
170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, 170 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
171 blacklistOnMyVideo: this.blacklistOnMyVideo, 171 blacklistOnMyVideo: this.blacklistOnMyVideo,
172 myVideoPublished: this.myVideoPublished, 172 myVideoPublished: this.myVideoPublished,
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 30985bb0f..2945bf709 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -1,22 +1,24 @@
1import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
2import { UserNotification, UserNotificationType } from '../../../shared' 4import { UserNotification, UserNotificationType } from '../../../shared'
3import { getSort, throwIfNotValid } from '../utils'
4import { isBooleanValid } from '../../helpers/custom-validators/misc' 5import { isBooleanValid } from '../../helpers/custom-validators/misc'
5import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' 6import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
6import { UserModel } from './user' 7import { AbuseModel } from '../abuse/abuse'
7import { VideoModel } from '../video/video' 8import { VideoAbuseModel } from '../abuse/video-abuse'
8import { VideoCommentModel } from '../video/video-comment' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10import { VideoChannelModel } from '../video/video-channel'
11import { AccountModel } from './account'
12import { VideoAbuseModel } from '../video/video-abuse'
13import { VideoBlacklistModel } from '../video/video-blacklist'
14import { VideoImportModel } from '../video/video-import'
15import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
16import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
17import { AvatarModel } from '../avatar/avatar' 12import { AvatarModel } from '../avatar/avatar'
18import { ServerModel } from '../server/server' 13import { ServerModel } from '../server/server'
19import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' 14import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video'
16import { VideoBlacklistModel } from '../video/video-blacklist'
17import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account'
21import { UserModel } from './user'
20 22
21enum ScopeNames { 23enum ScopeNames {
22 WITH_ALL = 'WITH_ALL' 24 WITH_ALL = 'WITH_ALL'
@@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) {
87 89
88 { 90 {
89 attributes: [ 'id' ], 91 attributes: [ 'id' ],
90 model: VideoAbuseModel.unscoped(), 92 model: AbuseModel.unscoped(),
91 required: false, 93 required: false,
92 include: [ buildVideoInclude(true) ] 94 include: [
95 {
96 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(),
98 required: false,
99 include: [ buildVideoInclude(true) ]
100 },
101 {
102 attributes: [ 'id' ],
103 model: VideoCommentAbuseModel.unscoped(),
104 required: false,
105 include: [
106 {
107 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel,
109 required: true,
110 include: [
111 {
112 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(),
114 required: true
115 }
116 ]
117 }
118 ]
119 },
120 {
121 model: AccountModel,
122 as: 'FlaggedAccount',
123 required: true,
124 include: [ buildActorWithAvatarInclude() ]
125 }
126 ]
93 }, 127 },
94 128
95 { 129 {
@@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
179 } 213 }
180 }, 214 },
181 { 215 {
182 fields: [ 'videoAbuseId' ], 216 fields: [ 'abuseId' ],
183 where: { 217 where: {
184 videoAbuseId: { 218 abuseId: {
185 [Op.ne]: null 219 [Op.ne]: null
186 } 220 }
187 } 221 }
@@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
276 }) 310 })
277 Comment: VideoCommentModel 311 Comment: VideoCommentModel
278 312
279 @ForeignKey(() => VideoAbuseModel) 313 @ForeignKey(() => AbuseModel)
280 @Column 314 @Column
281 videoAbuseId: number 315 abuseId: number
282 316
283 @BelongsTo(() => VideoAbuseModel, { 317 @BelongsTo(() => AbuseModel, {
284 foreignKey: { 318 foreignKey: {
285 allowNull: true 319 allowNull: true
286 }, 320 },
287 onDelete: 'cascade' 321 onDelete: 'cascade'
288 }) 322 })
289 VideoAbuse: VideoAbuseModel 323 Abuse: AbuseModel
290 324
291 @ForeignKey(() => VideoBlacklistModel) 325 @ForeignKey(() => VideoBlacklistModel)
292 @Column 326 @Column
@@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
397 video: this.formatVideo(this.Comment.Video) 431 video: this.formatVideo(this.Comment.Video)
398 } : undefined 432 } : undefined
399 433
400 const videoAbuse = this.VideoAbuse ? { 434 const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
401 id: this.VideoAbuse.id,
402 video: this.formatVideo(this.VideoAbuse.Video)
403 } : undefined
404 435
405 const videoBlacklist = this.VideoBlacklist ? { 436 const videoBlacklist = this.VideoBlacklist ? {
406 id: this.VideoBlacklist.id, 437 id: this.VideoBlacklist.id,
@@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
439 video, 470 video,
440 videoImport, 471 videoImport,
441 comment, 472 comment,
442 videoAbuse, 473 abuse,
443 videoBlacklist, 474 videoBlacklist,
444 account, 475 account,
445 actorFollow, 476 actorFollow,
@@ -456,6 +487,29 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
456 } 487 }
457 } 488 }
458 489
490 formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
491 const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? {
492 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
493
494 video: {
495 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
496 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
497 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
498 }
499 } : undefined
500
501 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
502
503 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
504
505 return {
506 id: abuse.id,
507 video: videoAbuse,
508 comment: commentAbuse,
509 account: accountAbuse
510 }
511 }
512
459 formatActor ( 513 formatActor (
460 this: UserNotificationModelForApi, 514 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor 515 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index de193131a..5f45f8e7c 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' 22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -168,28 +168,26 @@ enum ScopeNames {
168 '(' + 168 '(' +
169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
170 'FROM (' + 170 'FROM (' +
171 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + 171 'SELECT COUNT("abuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + 172 `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' + 173 'FROM "abuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + 174 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177 'WHERE "account"."userId" = "UserModel"."id"' + 175 'WHERE "account"."userId" = "UserModel"."id"' +
178 ') t' + 176 ') t' +
179 ')' 177 ')'
180 ), 178 ),
181 'videoAbusesCount' 179 'abusesCount'
182 ], 180 ],
183 [ 181 [
184 literal( 182 literal(
185 '(' + 183 '(' +
186 'SELECT COUNT("videoAbuse"."id") ' + 184 'SELECT COUNT("abuse"."id") ' +
187 'FROM "videoAbuse" ' + 185 'FROM "abuse" ' +
188 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + 186 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
189 'WHERE "account"."userId" = "UserModel"."id"' + 187 'WHERE "account"."userId" = "UserModel"."id"' +
190 ')' 188 ')'
191 ), 189 ),
192 'videoAbusesCreatedCount' 190 'abusesCreatedCount'
193 ], 191 ],
194 [ 192 [
195 literal( 193 literal(
@@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> {
780 const videoQuotaUsed = this.get('videoQuotaUsed') 778 const videoQuotaUsed = this.get('videoQuotaUsed')
781 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 779 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
782 const videosCount = this.get('videosCount') 780 const videosCount = this.get('videosCount')
783 const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') 781 const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
784 const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') 782 const abusesCreatedCount = this.get('abusesCreatedCount')
785 const videoCommentsCount = this.get('videoCommentsCount') 783 const videoCommentsCount = this.get('videoCommentsCount')
786 784
787 const json: User = { 785 const json: User = {
@@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> {
815 videosCount: videosCount !== undefined 813 videosCount: videosCount !== undefined
816 ? parseInt(videosCount + '', 10) 814 ? parseInt(videosCount + '', 10)
817 : undefined, 815 : undefined,
818 videoAbusesCount: videoAbusesCount 816 abusesCount: abusesCount
819 ? parseInt(videoAbusesCount, 10) 817 ? parseInt(abusesCount, 10)
820 : undefined, 818 : undefined,
821 videoAbusesAcceptedCount: videoAbusesAcceptedCount 819 abusesAcceptedCount: abusesAcceptedCount
822 ? parseInt(videoAbusesAcceptedCount, 10) 820 ? parseInt(abusesAcceptedCount, 10)
823 : undefined, 821 : undefined,
824 videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined 822 abusesCreatedCount: abusesCreatedCount !== undefined
825 ? parseInt(videoAbusesCreatedCount + '', 10) 823 ? parseInt(abusesCreatedCount + '', 10)
826 : undefined, 824 : undefined,
827 videoCommentsCount: videoCommentsCount !== undefined 825 videoCommentsCount: videoCommentsCount !== undefined
828 ? parseInt(videoCommentsCount + '', 10) 826 ? parseInt(videoCommentsCount + '', 10)
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 30f0525e5..68cd72ee7 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -1,11 +1,11 @@
1import * as Bluebird from 'bluebird'
2import { Op } from 'sequelize'
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 3import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
4import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
5import { ServerBlock } from '@shared/models'
2import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort, searchAttribute } from '../utils' 7import { getSort, searchAttribute } from '../utils'
6import * as Bluebird from 'bluebird' 8import { ServerModel } from './server'
7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
8import { Op } from 'sequelize'
9 9
10enum ScopeNames { 10enum ScopeNames {
11 WITH_ACCOUNT = 'WITH_ACCOUNT', 11 WITH_ACCOUNT = 'WITH_ACCOUNT',
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
deleted file mode 100644
index 1319332f0..000000000
--- a/server/models/video/video-abuse.ts
+++ /dev/null
@@ -1,479 +0,0 @@
1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
3import {
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
14 Table,
15 UpdatedAt
16} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import {
19 VideoAbuseState,
20 VideoDetails,
21 VideoAbusePredefinedReasons,
22 VideoAbusePredefinedReasonsString,
23 videoAbusePredefinedReasonsMap
24} from '../../../shared'
25import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
26import { VideoAbuse } from '../../../shared/models/videos'
27import {
28 isVideoAbuseModerationCommentValid,
29 isVideoAbuseReasonValid,
30 isVideoAbuseStateValid
31} from '../../helpers/custom-validators/video-abuses'
32import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
33import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
34import { AccountModel } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from './thumbnail'
37import { VideoModel } from './video'
38import { VideoBlacklistModel } from './video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
40import { invert } from 'lodash'
41
42export enum ScopeNames {
43 FOR_API = 'FOR_API'
44}
45
46@Scopes(() => ({
47 [ScopeNames.FOR_API]: (options: {
48 // search
49 search?: string
50 searchReporter?: string
51 searchReportee?: string
52 searchVideo?: string
53 searchVideoChannel?: string
54
55 // filters
56 id?: number
57 predefinedReasonId?: number
58
59 state?: VideoAbuseState
60 videoIs?: VideoAbuseVideoIs
61
62 // accountIds
63 serverAccountId: number
64 userAccountId: number
65 }) => {
66 const where = {
67 reporterAccountId: {
68 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
69 }
70 }
71
72 if (options.search) {
73 Object.assign(where, {
74 [Op.or]: [
75 {
76 [Op.and]: [
77 { videoId: { [Op.not]: null } },
78 searchAttribute(options.search, '$Video.name$')
79 ]
80 },
81 {
82 [Op.and]: [
83 { videoId: { [Op.not]: null } },
84 searchAttribute(options.search, '$Video.VideoChannel.name$')
85 ]
86 },
87 {
88 [Op.and]: [
89 { deletedVideo: { [Op.not]: null } },
90 { deletedVideo: searchAttribute(options.search, 'name') }
91 ]
92 },
93 {
94 [Op.and]: [
95 { deletedVideo: { [Op.not]: null } },
96 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
97 ]
98 },
99 searchAttribute(options.search, '$Account.name$')
100 ]
101 })
102 }
103
104 if (options.id) Object.assign(where, { id: options.id })
105 if (options.state) Object.assign(where, { state: options.state })
106
107 if (options.videoIs === 'deleted') {
108 Object.assign(where, {
109 deletedVideo: {
110 [Op.not]: null
111 }
112 })
113 }
114
115 if (options.predefinedReasonId) {
116 Object.assign(where, {
117 predefinedReasons: {
118 [Op.contains]: [ options.predefinedReasonId ]
119 }
120 })
121 }
122
123 const onlyBlacklisted = options.videoIs === 'blacklisted'
124
125 return {
126 attributes: {
127 include: [
128 [
129 // we don't care about this count for deleted videos, so there are not included
130 literal(
131 '(' +
132 'SELECT count(*) ' +
133 'FROM "videoAbuse" ' +
134 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
135 ')'
136 ),
137 'countReportsForVideo'
138 ],
139 [
140 // we don't care about this count for deleted videos, so there are not included
141 literal(
142 '(' +
143 'SELECT t.nth ' +
144 'FROM ( ' +
145 'SELECT id, ' +
146 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
147 'FROM "videoAbuse" ' +
148 ') t ' +
149 'WHERE t.id = "VideoAbuseModel".id ' +
150 ')'
151 ),
152 'nthReportForVideo'
153 ],
154 [
155 literal(
156 '(' +
157 'SELECT count("videoAbuse"."id") ' +
158 'FROM "videoAbuse" ' +
159 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
160 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
161 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
162 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
163 ')'
164 ),
165 'countReportsForReporter__video'
166 ],
167 [
168 literal(
169 '(' +
170 'SELECT count(DISTINCT "videoAbuse"."id") ' +
171 'FROM "videoAbuse" ' +
172 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
173 ')'
174 ),
175 'countReportsForReporter__deletedVideo'
176 ],
177 [
178 literal(
179 '(' +
180 'SELECT count(DISTINCT "videoAbuse"."id") ' +
181 'FROM "videoAbuse" ' +
182 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
183 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
184 'INNER JOIN "account" ON ' +
185 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
186 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
187 ')'
188 ),
189 'countReportsForReportee__video'
190 ],
191 [
192 literal(
193 '(' +
194 'SELECT count(DISTINCT "videoAbuse"."id") ' +
195 'FROM "videoAbuse" ' +
196 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
197 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
198 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
199 ')'
200 ),
201 'countReportsForReportee__deletedVideo'
202 ]
203 ]
204 },
205 include: [
206 {
207 model: AccountModel,
208 required: true,
209 where: searchAttribute(options.searchReporter, 'name')
210 },
211 {
212 model: VideoModel,
213 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
214 where: searchAttribute(options.searchVideo, 'name'),
215 include: [
216 {
217 model: ThumbnailModel
218 },
219 {
220 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
221 where: searchAttribute(options.searchVideoChannel, 'name'),
222 include: [
223 {
224 model: AccountModel,
225 where: searchAttribute(options.searchReportee, 'name')
226 }
227 ]
228 },
229 {
230 attributes: [ 'id', 'reason', 'unfederated' ],
231 model: VideoBlacklistModel,
232 required: onlyBlacklisted
233 }
234 ]
235 }
236 ],
237 where
238 }
239 }
240}))
241@Table({
242 tableName: 'videoAbuse',
243 indexes: [
244 {
245 fields: [ 'videoId' ]
246 },
247 {
248 fields: [ 'reporterAccountId' ]
249 }
250 ]
251})
252export class VideoAbuseModel extends Model<VideoAbuseModel> {
253
254 @AllowNull(false)
255 @Default(null)
256 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
257 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
258 reason: string
259
260 @AllowNull(false)
261 @Default(null)
262 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
263 @Column
264 state: VideoAbuseState
265
266 @AllowNull(true)
267 @Default(null)
268 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
269 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
270 moderationComment: string
271
272 @AllowNull(true)
273 @Default(null)
274 @Column(DataType.JSONB)
275 deletedVideo: VideoDetails
276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[]
281
282 @AllowNull(true)
283 @Default(null)
284 @Column
285 startAt: number
286
287 @AllowNull(true)
288 @Default(null)
289 @Column
290 endAt: number
291
292 @CreatedAt
293 createdAt: Date
294
295 @UpdatedAt
296 updatedAt: Date
297
298 @ForeignKey(() => AccountModel)
299 @Column
300 reporterAccountId: number
301
302 @BelongsTo(() => AccountModel, {
303 foreignKey: {
304 allowNull: true
305 },
306 onDelete: 'set null'
307 })
308 Account: AccountModel
309
310 @ForeignKey(() => VideoModel)
311 @Column
312 videoId: number
313
314 @BelongsTo(() => VideoModel, {
315 foreignKey: {
316 allowNull: true
317 },
318 onDelete: 'set null'
319 })
320 Video: VideoModel
321
322 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
323 const videoAttributes = {}
324 if (videoId) videoAttributes['videoId'] = videoId
325 if (uuid) videoAttributes['deletedVideo'] = { uuid }
326
327 const query = {
328 where: {
329 id,
330 ...videoAttributes
331 }
332 }
333 return VideoAbuseModel.findOne(query)
334 }
335
336 static listForApi (parameters: {
337 start: number
338 count: number
339 sort: string
340
341 serverAccountId: number
342 user?: MUserAccountId
343
344 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString
346 state?: VideoAbuseState
347 videoIs?: VideoAbuseVideoIs
348
349 search?: string
350 searchReporter?: string
351 searchReportee?: string
352 searchVideo?: string
353 searchVideoChannel?: string
354 }) {
355 const {
356 start,
357 count,
358 sort,
359 search,
360 user,
361 serverAccountId,
362 state,
363 videoIs,
364 predefinedReason,
365 searchReportee,
366 searchVideo,
367 searchVideoChannel,
368 searchReporter,
369 id
370 } = parameters
371
372 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
374
375 const query = {
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 col: 'VideoAbuseModel.id',
380 distinct: true
381 }
382
383 const filters = {
384 id,
385 predefinedReasonId,
386 search,
387 state,
388 videoIs,
389 searchReportee,
390 searchVideo,
391 searchVideoChannel,
392 searchReporter,
393 serverAccountId,
394 userAccountId
395 }
396
397 return VideoAbuseModel
398 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] }
400 ])
401 .findAndCountAll(query)
402 .then(({ rows, count }) => {
403 return { total: count, data: rows }
404 })
405 }
406
407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
409 const countReportsForVideo = this.get('countReportsForVideo') as number
410 const nthReportForVideo = this.get('nthReportForVideo') as number
411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
412 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
413 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
414 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
415
416 const video = this.Video
417 ? this.Video
418 : this.deletedVideo
419
420 return {
421 id: this.id,
422 reason: this.reason,
423 predefinedReasons,
424 reporterAccount: this.Account.toFormattedJSON(),
425 state: {
426 id: this.state,
427 label: VideoAbuseModel.getStateLabel(this.state)
428 },
429 moderationComment: this.moderationComment,
430 video: {
431 id: video.id,
432 uuid: video.uuid,
433 name: video.name,
434 nsfw: video.nsfw,
435 deleted: !this.Video,
436 blacklisted: this.Video?.isBlacklisted() || false,
437 thumbnailPath: this.Video?.getMiniatureStaticPath(),
438 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
439 },
440 createdAt: this.createdAt,
441 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
444 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0,
446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
447 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
448 }
449 }
450
451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
453
454 const startAt = this.startAt
455 const endAt = this.endAt
456
457 return {
458 type: 'Flag' as 'Flag',
459 content: this.reason,
460 object: this.Video.url,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
467 }
468 }
469
470 private static getStateLabel (id: number) {
471 return VIDEO_ABUSE_STATES[id] || 'Unknown'
472 }
473
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
475 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
478 }
479}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9cee64229..03a3cdf81 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -61,6 +61,7 @@ type AvailableWithStatsOptions = {
61} 61}
62 62
63export type SummaryOptions = { 63export type SummaryOptions = {
64 actorRequired?: boolean // Default: true
64 withAccount?: boolean // Default: false 65 withAccount?: boolean // Default: false
65 withAccountBlockerIds?: number[] 66 withAccountBlockerIds?: number[]
66} 67}
@@ -121,7 +122,7 @@ export type SummaryOptions = {
121 { 122 {
122 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 123 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
123 model: ActorModel.unscoped(), 124 model: ActorModel.unscoped(),
124 required: true, 125 required: options.actorRequired ?? true,
125 include: [ 126 include: [
126 { 127 {
127 attributes: [ 'host' ], 128 attributes: [ 'host' ],
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 90625d987..75b914b8c 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,7 +1,20 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { uniq } from 'lodash' 2import { uniq } from 'lodash'
3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' 3import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 4import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 ForeignKey,
11 HasMany,
12 Is,
13 Model,
14 Scopes,
15 Table,
16 UpdatedAt
17} from 'sequelize-typescript'
5import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
6import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 19import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
7import { VideoPrivacy } from '@shared/models' 20import { VideoPrivacy } from '@shared/models'
@@ -24,6 +37,7 @@ import {
24 MCommentOwnerVideoReply, 37 MCommentOwnerVideoReply,
25 MVideoImmutable 38 MVideoImmutable
26} from '../../types/models/video' 39} from '../../types/models/video'
40import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
27import { AccountModel } from '../account/account' 41import { AccountModel } from '../account/account'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 42import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' 43import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
@@ -224,6 +238,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
224 }) 238 })
225 Account: AccountModel 239 Account: AccountModel
226 240
241 @HasMany(() => VideoCommentAbuseModel, {
242 foreignKey: {
243 name: 'videoCommentId',
244 allowNull: true
245 },
246 onDelete: 'set null'
247 })
248 CommentAbuses: VideoCommentAbuseModel[]
249
227 static loadById (id: number, t?: Transaction): Bluebird<MComment> { 250 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
228 const query: FindOptions = { 251 const query: FindOptions = {
229 where: { 252 where: {
@@ -632,7 +655,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
632 id: this.id, 655 id: this.id,
633 url: this.url, 656 url: this.url,
634 text: this.text, 657 text: this.text,
635 threadId: this.originCommentId || this.id, 658 threadId: this.getThreadId(),
636 inReplyToCommentId: this.inReplyToCommentId || null, 659 inReplyToCommentId: this.inReplyToCommentId || null,
637 videoId: this.videoId, 660 videoId: this.videoId,
638 createdAt: this.createdAt, 661 createdAt: this.createdAt,
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 984b0e6af..466890364 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -327,7 +327,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
327 attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') 327 attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"')
328 } 328 }
329 329
330 order = buildOrder(model, options.sort) 330 order = buildOrder(options.sort)
331 suffix += `${order} ` 331 suffix += `${order} `
332 } 332 }
333 333
@@ -357,7 +357,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
357 return { query, replacements, order } 357 return { query, replacements, order }
358} 358}
359 359
360function buildOrder (model: typeof Model, value: string) { 360function buildOrder (value: string) {
361 const { direction, field } = buildDirectionAndField(value) 361 const { direction, field } = buildDirectionAndField(value)
362 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) 362 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field)
363 363
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e2718300e..43609587c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,4 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { remove } from 'fs-extra'
2import { maxBy, minBy, pick } from 'lodash' 3import { maxBy, minBy, pick } from 'lodash'
3import { join } from 'path' 4import { join } from 'path'
4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 5import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
@@ -23,10 +24,18 @@ import {
23 Table, 24 Table,
24 UpdatedAt 25 UpdatedAt
25} from 'sequelize-typescript' 26} from 'sequelize-typescript'
26import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
29import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
30import { getServerActor } from '@server/models/application/application'
31import { ModelCache } from '@server/models/model-cache'
32import { VideoFile } from '@shared/models/videos/video-file.model'
33import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
27import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 34import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
28import { Video, VideoDetails } from '../../../shared/models/videos' 35import { Video, VideoDetails } from '../../../shared/models/videos'
36import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
29import { VideoFilter } from '../../../shared/models/videos/video-query.type' 37import { VideoFilter } from '../../../shared/models/videos/video-query.type'
38import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
30import { peertubeTruncate } from '../../helpers/core-utils' 39import { peertubeTruncate } from '../../helpers/core-utils'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 40import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { isBooleanValid } from '../../helpers/custom-validators/misc' 41import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -43,6 +52,7 @@ import {
43} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 53import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
45import { logger } from '../../helpers/logger' 54import { logger } from '../../helpers/logger'
55import { CONFIG } from '../../initializers/config'
46import { 56import {
47 ACTIVITY_PUB, 57 ACTIVITY_PUB,
48 API_VERSION, 58 API_VERSION,
@@ -59,40 +69,6 @@ import {
59 WEBSERVER 69 WEBSERVER
60} from '../../initializers/constants' 70} from '../../initializers/constants'
61import { sendDeleteVideo } from '../../lib/activitypub/send' 71import { sendDeleteVideo } from '../../lib/activitypub/send'
62import { AccountModel } from '../account/account'
63import { AccountVideoRateModel } from '../account/account-video-rate'
64import { ActorModel } from '../activitypub/actor'
65import { AvatarModel } from '../avatar/avatar'
66import { ServerModel } from '../server/server'
67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
68import { TagModel } from './tag'
69import { VideoAbuseModel } from './video-abuse'
70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
71import { VideoCommentModel } from './video-comment'
72import { VideoFileModel } from './video-file'
73import { VideoShareModel } from './video-share'
74import { VideoTagModel } from './video-tag'
75import { ScheduleVideoUpdateModel } from './schedule-video-update'
76import { VideoCaptionModel } from './video-caption'
77import { VideoBlacklistModel } from './video-blacklist'
78import { remove } from 'fs-extra'
79import { VideoViewModel } from './video-view'
80import { VideoRedundancyModel } from '../redundancy/video-redundancy'
81import {
82 videoFilesModelToFormattedJSON,
83 VideoFormattingJSONOptions,
84 videoModelToActivityPubObject,
85 videoModelToFormattedDetailsJSON,
86 videoModelToFormattedJSON
87} from './video-format-utils'
88import { UserVideoHistoryModel } from '../account/user-video-history'
89import { VideoImportModel } from './video-import'
90import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
91import { VideoPlaylistElementModel } from './video-playlist-element'
92import { CONFIG } from '../../initializers/config'
93import { ThumbnailModel } from './thumbnail'
94import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
95import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
96import { 72import {
97 MChannel, 73 MChannel,
98 MChannelAccountDefault, 74 MChannelAccountDefault,
@@ -118,15 +94,39 @@ import {
118 MVideoWithFile, 94 MVideoWithFile,
119 MVideoWithRights 95 MVideoWithRights
120} from '../../types/models' 96} from '../../types/models'
121import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
122import { MThumbnail } from '../../types/models/video/thumbnail' 97import { MThumbnail } from '../../types/models/video/thumbnail'
123import { VideoFile } from '@shared/models/videos/video-file.model' 98import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 99import { VideoAbuseModel } from '../abuse/video-abuse'
125import { ModelCache } from '@server/models/model-cache' 100import { AccountModel } from '../account/account'
101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { UserVideoHistoryModel } from '../account/user-video-history'
103import { ActorModel } from '../activitypub/actor'
104import { AvatarModel } from '../avatar/avatar'
105import { VideoRedundancyModel } from '../redundancy/video-redundancy'
106import { ServerModel } from '../server/server'
107import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
108import { ScheduleVideoUpdateModel } from './schedule-video-update'
109import { TagModel } from './tag'
110import { ThumbnailModel } from './thumbnail'
111import { VideoBlacklistModel } from './video-blacklist'
112import { VideoCaptionModel } from './video-caption'
113import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
114import { VideoCommentModel } from './video-comment'
115import { VideoFileModel } from './video-file'
116import {
117 videoFilesModelToFormattedJSON,
118 VideoFormattingJSONOptions,
119 videoModelToActivityPubObject,
120 videoModelToFormattedDetailsJSON,
121 videoModelToFormattedJSON
122} from './video-format-utils'
123import { VideoImportModel } from './video-import'
124import { VideoPlaylistElementModel } from './video-playlist-element'
126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' 125import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
127import { buildNSFWFilter } from '@server/helpers/express-utils' 126import { VideoShareModel } from './video-share'
128import { getServerActor } from '@server/models/application/application' 127import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
129import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" 128import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view'
130 130
131export enum ScopeNames { 131export enum ScopeNames {
132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> {
803 static async saveEssentialDataToAbuses (instance: VideoModel, options) { 803 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
804 const tasks: Promise<any>[] = [] 804 const tasks: Promise<any>[] = []
805 805
806 logger.info('Saving video abuses details of video %s.', instance.url)
807
808 if (!Array.isArray(instance.VideoAbuses)) { 806 if (!Array.isArray(instance.VideoAbuses)) {
809 instance.VideoAbuses = await instance.$get('VideoAbuses') 807 instance.VideoAbuses = await instance.$get('VideoAbuses')
810 808
811 if (instance.VideoAbuses.length === 0) return undefined 809 if (instance.VideoAbuses.length === 0) return undefined
812 } 810 }
813 811
812 logger.info('Saving video abuses details of video %s.', instance.url)
813
814 const details = instance.toFormattedDetailsJSON() 814 const details = instance.toFormattedDetailsJSON()
815 815
816 for (const abuse of instance.VideoAbuses) { 816 for (const abuse of instance.VideoAbuses) {